Build an AI Triage Routing Console

Build a local console for explainable emergency routing and human oversight.

Introduction

30 Second Summary

During a busy emergency shift, the closest hospital can look like the obvious destination. That choice can fail when its beds are full or the required service is missing.

In this project, you will build a browser-based Control Centre console for a fictional hospital network. You will compare the nearest hospital with an explainable recommendation before approving or overriding the route.

What You'll Build

You'll select a fictional emergency case and watch the console expose a flawed nearest-only route before presenting an explainable destination for human review.

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

  • A live operations board powered by synthetic data. You'll see three case suggestions beside four hospitals with changing bed counts.
  • A constraint-aware routing recommendation that exposes both failure reasons behind the nearest-hospital choice. You'll see travel time and the capacity penalty combine into a simulation score.
  • A human approval workflow for approving or overriding a destination. You'll see fictional capacity and the audit log update immediately.
  • Secret Mission: Run a capacity-collapse drill until no eligible route remains. Identify the point where the Control Centre must escalate.

Do I need any experience before starting?

You'll need a 64-bit Windows computer. No previous web development setup is required because the first step verifies Visual Studio Code and Microsoft Edge.

Before We Start

Before you start building, lock in the purpose of your local browser-based Emergency Control Centre simulation. You will show fictional upstream AI triage suggestions while keeping every simulated dispatch decision under human review.

Set Up the Windows Workspace

Your Control Centre runs entirely in the browser. You still need a reliable place to create its three local files.

In this step, you'll verify Visual Studio Code from a fresh terminal. You'll also confirm that Microsoft Edge is ready for browser testing.

In this step, get ready to:
  • Verify Visual Studio Code through its command-line version output.
  • Confirm Microsoft Edge opens for local browser testing.
  • Create the empty emergency-control-centre workspace on your Desktop.
Verify your editor and browser

The Visual Studio Code User setup adds its command to %PATH%. This lets a terminal find the editor from any folder.

  • Press the Windows key to open Start search.
  • Type Command Prompt into the search field.
  • Press Enter to open a fresh terminal.

Before you run the check, do you expect version details or an unavailable command?

  • Check whether the Visual Studio Code command is available by running:
code --version

What does this check prove?

  • A version number confirms that the editor command works.
  • A commit ID identifies the installed build.
  • An architecture line identifies the build type.

✔️ I see version information

Good start. Visual Studio Code is available from a fresh terminal.

ⓧ No version information appears

The Windows User setup installs Visual Studio Code without administrator permissions. It also adds the editor command to %PATH%.

  • Open the official Visual Studio Code download page.
  • Select the User Installer link for your computer's 64-bit architecture.
  • Run the downloaded installer.
  • Follow the installer prompts to finish the User setup.
  • Close every Command Prompt window.
  • Open a fresh Command Prompt window from Start search.
  • Verify the installation by running:
code --version

What should you see?

You should see the installed version number. The next lines show the commit ID plus the architecture.

Still missing version information?

Make sure every terminal window from before the installation is closed. A new terminal reads the updated %PATH% value.

Ask for help: Help me troubleshoot why the Visual Studio Code command is unavailable after Windows User setup.

Microsoft Edge provides the local browser where you test each visible dashboard change.

  • Press the Windows key to return to Start search.
  • Type Microsoft Edge into the search field.
  • Press Enter to start the browser.

✔️ Microsoft Edge opens

Your browser window is open. Microsoft Edge is ready to test the local Control Centre.

ⓧ Microsoft Edge is unavailable

Microsoft provides an official Windows installer for Microsoft Edge.

  • Open the official Microsoft Edge download page.
  • Select Download Edge for Windows.
  • Run the downloaded installer.
  • Return to Start search.
  • Search for Microsoft Edge.
  • Press Enter to start the installed browser.

You should see a Microsoft Edge browser window.

Create the Desktop project folder

A single folder gives your dashboard files a predictable home. You'll create it with Windows File Explorer.

  • Press Windows key+E to open File Explorer.
  • Select Desktop in the left navigation pane.
  • Select New in the File Explorer toolbar.
  • Select Folder.
  • Type emergency-control-centre as the folder name.
  • Press Enter to save the folder name.

You should see the empty emergency-control-centre folder on your Desktop.

Can't find the folder?

  • Check that Desktop was selected before you created the folder.
  • Confirm that the folder name is exactly emergency-control-centre.

Ask for help: Help me locate the emergency-control-centre folder I created with Windows File Explorer.

Open the folder as your workspace

File Explorer's address bar can launch Command Prompt inside the folder you are viewing. The new terminal starts at that folder's path.

  • Double-click the emergency-control-centre folder to open it.
  • Click the File Explorer address bar.
  • Type cmd into the address bar.
  • Press Enter to open Command Prompt in this folder.

The terminal prompt path should end with Desktop\emergency-control-centre.

Before you run the final command, do you expect Visual Studio Code to open a file or the current folder?

  • Open the current folder as your Visual Studio Code workspace by running:
code .

What does this command do?

  • The code command launches Visual Studio Code through %PATH%.
  • The . value represents the current folder.
  • Visual Studio Code uses that folder as the workspace.

That's the setup sorted. Visual Studio Code opens with emergency-control-centre in the Explorer sidebar.

The workspace is empty because no project code files exist yet.

Did the wrong workspace open?

  • Close the incorrect Visual Studio Code window.
  • Return to the open emergency-control-centre folder in File Explorer.
  • Repeat the address-bar method from that folder.

Ask for help: Help me confirm that Visual Studio Code opened my Desktop project folder as the current workspace.

Your editor workspace is verified. Next, you'll turn this empty folder into the first visible Control Centre operations board.

Build the Live Operations Board

The verified Visual Studio Code workspace gives you a reliable place to build. The Control Centre now needs an immediate picture of incoming cases before routing becomes useful.

You will structure the board with HTML. You will add a responsive layout with CSS. You will use JavaScript to display synthetic data in Microsoft Edge.

In this step, get ready to:
  • Create the dashboard structure in index.html.
  • Style the responsive operations board in styles.css.
  • Render three fictional cases plus four fictional hospitals from app.js.
Build the dashboard structure

The document needs a stable container for every piece of operational information. Later functions can target these containers without rebuilding the page structure.

  • Create index.html inside the open emergency-control-centre workspace using the file creation icon above the workspace file tree.

You should see index.html in the workspace file tree.

  • Add the document shell plus educational warning to index.html by pasting this code:
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Emergency Network Control Centre</title>
  <link rel="stylesheet" href="styles.css">
  <script src="app.js" defer></script>
</head>
<body>
  <header class="topbar">
    <div>
      <p class="eyebrow">Hospital Network Operations</p>
      <h1>Emergency Control Centre</h1>
    </div>
    <span class="simulation-badge">Educational simulation | Synthetic data</span>
  </header>

  <main class="dashboard">
    <section class="panel span-2 notice">
      <strong>Not for clinical use.</strong>
      This prototype displays fictional upstream AI suggestions and a project-defined routing heuristic. A human operator makes the final simulated decision.
    </section>
  </main>
</body>
</html>

What does this structure do?

  • The header names the fictional hospital network console.
  • The warning identifies the page as an educational simulation.
  • The linked files connect the document to its future styles plus rendering logic.
  • Save index.html.
  • Right-click index.html in the Visual Studio Code workspace file tree.
  • Choose the option that shows the file in Windows File Explorer.
  • Right-click index.html in Windows File Explorer.
  • Choose Microsoft Edge as the application that opens the file.

Does the page stay blank?

Confirm that the file is named index.html. Save the file before opening it again.

Ask for help: Help me find why my local Control Centre page is blank.

  • Return to index.html in Visual Studio Code.
  • Insert the case queue immediately above the closing </main> tag by pasting this code:
    <section class="panel">
      <div class="section-heading">
        <div>
          <p class="eyebrow">Incoming</p>
          <h2>Case queue</h2>
        </div>
      </div>
      <div id="case-list" class="case-list"></div>
    </section>

What does the queue need?

The case-list container gives the rendering code a destination. Each synthetic case becomes a selectable button inside it.

  • Save index.html.
  • Switch back to Microsoft Edge.
  • Refresh the page.

You'll see a Case queue heading below the warning.

  • Insert the review panel below the case queue by pasting this code:
    <section class="panel">
      <div class="section-heading">
        <div>
          <p class="eyebrow">Review</p>
          <h2>Selected case</h2>
        </div>
      </div>
      <div id="selected-case"></div>

      <div class="route-grid">
        <article class="route-card">
          <h3>Naive nearest route</h3>
          <div id="naive-route"></div>
        </article>
        <article class="route-card recommended">
          <h3>Network-aware route</h3>
          <div id="smart-route"></div>
        </article>
      </div>

      <div class="operator-controls">
        <button id="approve-route" type="button">Approve recommendation</button>
        <select id="override-hospital" aria-label="Override destination"></select>
        <button id="override-route" class="secondary" type="button">Override route</button>
      </div>
    </section>

How is the review panel organized?

  • The selected-case container holds the case under review.
  • The route cards separate the nearest-only result from the network-aware result.
  • The controls reserve approval plus override actions for the human operator.
  • Save index.html.
  • Switch back to Microsoft Edge.
  • Refresh the page.

You'll see the Selected case panel with two route cards. You'll also see three operator controls.

  • Insert the hospital-capacity section below the review panel by pasting this code:
    <section class="panel span-2">
      <div class="section-heading">
        <div>
          <p class="eyebrow">Live network</p>
          <h2>Hospital capacity</h2>
        </div>
      </div>
      <div class="table-wrap">
        <table>
          <thead>
            <tr>
              <th>Hospital</th>
              <th>Travel</th>
              <th>Available beds</th>
              <th>Services</th>
            </tr>
          </thead>
          <tbody id="hospital-body"></tbody>
        </table>
      </div>
    </section>

What will the table compare?

The fixed headings show the facts used during routing. The hospital-body container receives one row for each fictional hospital.

  • Save index.html.
  • Switch back to Microsoft Edge.
  • Refresh the page.

You'll see the Hospital capacity table headings for travel time. You'll also see headings for beds plus services.

  • Insert the audit-log section below the hospital-capacity section by pasting this code:
    <section class="panel span-2">
      <div class="section-heading">
        <div>
          <p class="eyebrow">Traceability</p>
          <h2>Decision audit log</h2>
        </div>
      </div>
      <ul id="decision-log" class="decision-log"></ul>
    </section>

Why reserve an audit log?

The decision-log container provides a visible record for future operator decisions. That record makes each simulated dispatch traceable.

  • Insert the fictional-data footer immediately above the closing </body> tag by pasting this code:
  <footer>
    Fictional hospitals, travel times, cases, services, and capacities are used throughout.
  </footer>

What does the footer clarify?

The footer keeps the scope visible at the bottom of the page. Every case plus hospital value in this project is fictional.

  • Save index.html.
  • Switch back to Microsoft Edge.
  • Refresh the page.

You'll see the Decision audit log heading above the fictional-data footer.

Is a section outside the dashboard?

Keep every dashboard section above the closing </main> tag. Keep the footer below that tag.

Ask for help: Help me check the section order in index.html.

Style the operations board

The page now has every structural region it needs. Styling turns those regions into a dashboard that stays readable across wide plus narrow browser windows.

  • Create styles.css inside the open emergency-control-centre workspace using the file creation icon above the workspace file tree.

You should see styles.css beside index.html in the workspace file tree.

  • Add the page foundations plus top bar to styles.css by pasting this code:
:root {
  color-scheme: light;
  font-family: Inter, "Segoe UI", Arial, sans-serif;
  background: #eef2f7;
  color: #172033;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
}

button,
select {
  font: inherit;
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.25rem clamp(1rem, 4vw, 3rem);
  color: white;
  background: #10243e;
}

What do these foundations control?

  • The root rules establish the page colors plus typeface.
  • The universal sizing rule keeps padding inside each element's width.
  • The top bar separates the console identity from the operational panels.
  • Save styles.css.
  • Switch back to Microsoft Edge.
  • Refresh the page.

You'll see a pale page background beneath a dark blue header.

Are the styles missing?

Confirm that styles.css sits beside index.html. Check that both files are saved.

Ask for help: Help me find why styles.css is not affecting my page.

  • Add the heading hierarchy plus eyebrow label styles by pasting this code at the end of styles.css:
h1,
h2,
h3,
p {
  margin-top: 0;
}

h1 {
  margin-bottom: 0;
  font-size: clamp(1.5rem, 4vw, 2.25rem);
}

h2 {
  margin-bottom: 0;
  font-size: 1.1rem;
}

h3 {
  font-size: 0.95rem;
}

.eyebrow {
  margin-bottom: 0.3rem;
  color: #6b7a90;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

Why use a heading hierarchy?

The heading sizes separate page-level information from panel labels. Eyebrow text gives each operational area a compact category label.

  • Save styles.css.
  • Switch back to Microsoft Edge.
  • Refresh the page.

You'll see compact panel headings beneath uppercase eyebrow labels.

  • Add the simulation badge plus dashboard grid by pasting this code:
.topbar .eyebrow {
  color: #9fc3eb;
}

.simulation-badge {
  padding: 0.55rem 0.8rem;
  border: 1px solid #4d6f94;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 700;
}

.dashboard {
  display: grid;
  grid-template-columns: minmax(260px, 0.8fr) minmax(420px, 1.6fr);
  gap: 1rem;
  width: min(1200px, calc(100% - 2rem));
  margin: 1rem auto;
}

How does the dashboard grid work?

The grid gives the queue a narrower column. The wider column leaves more room for route comparisons.

  • Save styles.css.
  • Switch back to Microsoft Edge.
  • Refresh the page.

You'll see the simulation label become a pill-shaped badge. The dashboard content now uses two columns.

  • Add the panel plus warning styles by pasting this code:
.panel {
  padding: 1rem;
  border: 1px solid #dce3ec;
  border-radius: 14px;
  background: white;
  box-shadow: 0 8px 24px rgba(16, 36, 62, 0.06);
}

.span-2 {
  grid-column: 1 / -1;
}

.notice {
  border-left: 5px solid #e2a100;
  background: #fff9e9;
}

.section-heading {
  display: flex;
  justify-content: space-between;
  margin-bottom: 0.8rem;
}

.case-list {
  display: grid;
  gap: 0.7rem;
}

What changes in the board?

Each operational region becomes a white panel. The warning spans both columns with a yellow accent.

  • Save styles.css.
  • Switch back to Microsoft Edge.
  • Refresh the page.

You'll see rounded white panels. The warning now stretches across the dashboard.

  • Add the selectable case-button styles by pasting this code:
.case-button {
  width: 100%;
  padding: 0.8rem;
  border: 1px solid #dce3ec;
  border-radius: 10px;
  color: inherit;
  background: white;
  text-align: left;
  cursor: pointer;
}

.case-button:hover,
.case-button.active {
  border-color: #2d69a5;
  background: #f2f7fc;
}

.case-topline,
.selected-topline {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

How will a selected case stand out?

Each case fills the queue width as a button. The active case receives a blue border plus a pale background.

  • Save styles.css.
  • Switch back to Microsoft Edge.
  • Refresh the page.

The queue remains empty until the script renders its cases. Its container is ready for full-width case controls.

  • Add the case metadata plus triage badge styles by pasting this code:
.case-meta,
.muted {
  color: #627087;
  font-size: 0.82rem;
}

.triage {
  display: inline-block;
  padding: 0.25rem 0.55rem;
  border-radius: 999px;
  color: white;
  font-size: 0.7rem;
  font-weight: 800;
}

.triage.red {
  background: #c62828;
}

.triage.yellow {
  color: #3d2c00;
  background: #f6c344;
}

.triage.green {
  background: #17824b;
}

How do the triage signals work?

Each triage category receives a distinct badge color. Supporting case information uses muted text so the category remains easy to scan.

  • Save styles.css.
  • Switch back to Microsoft Edge.
  • Refresh the page.

The red plus yellow plus green badge styles are ready for the synthetic cases.

  • Add the side-by-side route-card styles by pasting this code:
.route-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.8rem;
  margin-top: 1rem;
}

.route-card {
  min-height: 150px;
  padding: 0.9rem;
  border: 1px solid #dce3ec;
  border-radius: 10px;
  background: #f8fafc;
}

.route-card.recommended {
  border-color: #8ab6df;
  background: #eff7ff;
}

Why separate the routes visually?

The two-column layout makes the route approaches easy to compare. The blue-tinted card gives the network-aware result a consistent home.

  • Save styles.css.
  • Switch back to Microsoft Edge.
  • Refresh the page.

You'll see two equal route cards inside the review panel.

  • Add the route-result states plus operator-control layout by pasting this code:
.warning {
  color: #a43b22;
  font-weight: 700;
}

.success {
  color: #176b40;
  font-weight: 700;
}

.operator-controls {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 0.6rem;
  margin-top: 1rem;
}

button,
select {
  min-height: 42px;
  padding: 0.65rem 0.75rem;
  border: 1px solid #2d69a5;
  border-radius: 8px;
}

What do these states prepare?

Warning plus success text will distinguish failed routes from eligible routes. The controls occupy three equal columns beneath the comparison.

  • Save styles.css.
  • Switch back to Microsoft Edge.
  • Refresh the page.

You'll see the three operator controls aligned beneath the route cards.

  • Add the button plus table foundations by pasting this code:
button {
  color: white;
  background: #2d69a5;
  font-weight: 750;
  cursor: pointer;
}

button.secondary {
  color: #2d69a5;
  background: white;
}

button:disabled,
select:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

.table-wrap {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
}

How are the controls distinguished?

The approval control uses a filled button. The override control uses an outlined button so its purpose remains distinct.

  • Save styles.css.
  • Switch back to Microsoft Edge.
  • Refresh the page.

You'll see a filled approval button plus an outlined override button.

  • Add the table-cell plus full-capacity styles by pasting this code:
th,
td {
  padding: 0.75rem;
  border-bottom: 1px solid #e4e9f0;
  text-align: left;
}

th {
  color: #627087;
  font-size: 0.76rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.full {
  color: #b3261e;
  font-weight: 800;
}

How will the table show capacity?

Spacing keeps each hospital row readable. The full class highlights zero-bed capacity in red.

  • Save styles.css.
  • Switch back to Microsoft Edge.
  • Refresh the page.

You'll see uppercase table headings with clear spacing between columns.

  • Add the audit-log plus footer styles by pasting this code:
.decision-log {
  display: grid;
  gap: 0.6rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.decision-log li {
  padding: 0.75rem;
  border-left: 4px solid #2d69a5;
  border-radius: 6px;
  background: #f5f8fc;
}

footer {
  padding: 0.5rem 1rem 1.5rem;
  color: #627087;
  font-size: 0.75rem;
  text-align: center;
}

What will an audit entry look like?

Each future decision receives a blue traceability marker. The footer keeps the fictional-data reminder centered beneath the dashboard.

  • Save styles.css.
  • Switch back to Microsoft Edge.
  • Refresh the page.

You'll see the fictional-data reminder centered beneath the dashboard.

  • Add the narrow-window layout rules by pasting this code:
@media (max-width: 800px) {
  .topbar,
  .case-topline,
  .selected-topline {
    align-items: flex-start;
    flex-direction: column;
  }

  .dashboard,
  .route-grid,
  .operator-controls {
    grid-template-columns: 1fr;
  }

  .span-2 {
    grid-column: auto;
  }
}

How does the board respond?

The media query switches multi-column regions to a single column on narrow screens. This keeps each panel plus control readable.

  • Save styles.css.
  • Switch back to Microsoft Edge.
  • Narrow the browser window.

You'll see the dashboard panels stack into one column. The route cards plus operator controls stack as well.

Does the layout stay in two columns?

Confirm that the media query appears at the end of styles.css. Check that every brace has a matching closing brace.

Ask for help: Help me debug the responsive dashboard layout.

Render the live network data

The styled board still needs operational content. Two arrays will hold the fictional network state that the rendering functions turn into visible controls plus table rows.

  • Create app.js inside the open emergency-control-centre workspace using the file creation icon above the workspace file tree.

You should see app.js beside the other two files.

  • Define the four fictional hospitals in app.js by pasting this code:
const hospitals = [
  {
    id: "central",
    name: "Central General",
    travelMinutes: 7,
    beds: 0,
    services: ["General", "Cardiac"],
  },
  {
    id: "riverside",
    name: "Riverside Trauma",
    travelMinutes: 12,
    beds: 1,
    services: ["Trauma", "General"],
  },
  {
    id: "north-heart",
    name: "North Heart Institute",
    travelMinutes: 18,
    beds: 1,
    services: ["Cardiac"],
  },
  {
    id: "lakeside",
    name: "Lakeside Medical",
    travelMinutes: 14,
    beds: 1,
    services: ["General"],
  },
];

What does the hospital data represent?

  • Each hospital has a stable identifier plus a display name.
  • Travel minutes provide the fictional proximity input.
  • Beds plus services represent the network constraints.
  • Add the three fictional cases below the hospitals array by pasting this code:
const cases = [
  {
    id: "ER-104",
    ageGroup: "Adult",
    triage: "Red",
    summary: "Synthetic high-acuity trauma scenario",
    requiredService: "Trauma",
    aiReason: "Upstream simulation flagged immediate review.",
    status: "Pending",
  },
  {
    id: "ER-105",
    ageGroup: "Adult",
    triage: "Yellow",
    summary: "Synthetic moderate-acuity cardiac scenario",
    requiredService: "Cardiac",
    aiReason: "Upstream simulation flagged review soon.",
    status: "Pending",
  },
  {
    id: "ER-106",
    ageGroup: "Adult",
    triage: "Green",
    summary: "Synthetic low-acuity general-care scenario",
    requiredService: "General",
    aiReason: "Upstream simulation marked the case able to wait.",
    status: "Pending",
  },
];

What does each case carry?

Each case records a triage category plus a required service. Its pending status shows that no simulated dispatch has happened.

  • Save app.js.
  • Switch back to Microsoft Edge.
  • Refresh the page.
  • Press Ctrl+Shift+J to open the Console.
  • Display the loaded hospital data by entering this expression:
hospitals

What does this check prove?

The Console displays an array containing four hospital objects. This proves that Microsoft Edge loaded the script plus its network data.

  • Display the loaded case data by entering this expression:
cases

What should the Console show?

The Console displays an array containing three case objects. Their identifiers are ER-104, ER-105, plus ER-106.

Is an array unavailable?

Confirm that app.js is saved beside index.html. Refresh the page before entering the expression again.

Ask for help: Help me debug the synthetic data arrays in app.js.

  • Add the dashboard state plus element references below the arrays by pasting this code:
let selectedCaseId = "ER-104";
const decisionLog = [];

const caseList = document.querySelector("#case-list");
const selectedCasePanel = document.querySelector("#selected-case");
const naiveRoutePanel = document.querySelector("#naive-route");
const smartRoutePanel = document.querySelector("#smart-route");
const hospitalBody = document.querySelector("#hospital-body");
const decisionLogPanel = document.querySelector("#decision-log");
const approveRouteButton = document.querySelector("#approve-route");
const overrideHospitalSelect = document.querySelector("#override-hospital");
const overrideRouteButton = document.querySelector("#override-route");

function getSelectedCase() {
  return cases.find((patientCase) => patientCase.id === selectedCaseId);
}

How does the script connect to the page?

  • The starting selection points to ER-104.
  • The query constants connect the script to matching HTML containers plus controls.
  • The helper returns the case that matches the current selection.
  • Add the case-queue renderer below getSelectedCase() by pasting this code:
function renderCaseList() {
  caseList.innerHTML = cases
    .map(
      (patientCase) => `
        <button
          class="case-button ${patientCase.id === selectedCaseId ? "active" : ""}"
          data-case-id="${patientCase.id}"
          type="button"
        >
          <span class="case-topline">
            <strong>${patientCase.id}</strong>
            <span class="triage ${patientCase.triage.toLowerCase()}">${patientCase.triage}</span>
          </span>
          <span class="case-meta">${patientCase.requiredService} | ${patientCase.status}</span>
        </button>
      `,
    )
    .join("");

  document.querySelectorAll("[data-case-id]").forEach((button) => {
    button.addEventListener("click", () => {
      selectedCaseId = button.dataset.caseId;
      renderAll();
    });
  });
}

How does the queue renderer work?

  • The mapping operation turns each case object into a button.
  • The selected identifier adds the active style to one case.
  • Each case button stores its identifier for future selection changes.
  • Save app.js.
  • Switch back to the Microsoft Edge Console.
  • Render the case queue by entering this expression:
renderCaseList()

What does this render prove?

The function fills the queue with three case buttons. Their red plus yellow plus green badges come from the stylesheet.

You'll see ER-104 selected in the queue. Its active styling marks the initial case.

  • Add the hospital renderer plus initial render function below renderCaseList() by pasting this code:
function renderHospitals() {
  hospitalBody.innerHTML = hospitals
    .map(
      (hospital) => `
        <tr>
          <td><strong>${hospital.name}</strong></td>
          <td>${hospital.travelMinutes} min</td>
          <td class="${hospital.beds === 0 ? "full" : ""}">${hospital.beds}</td>
          <td>${hospital.services.join(", ")}</td>
        </tr>
      `,
    )
    .join("");
}

function renderAll() {
  renderCaseList();
  renderHospitals();
}

renderAll();

How does the first live render work?

  • The hospital renderer converts every hospital object into one table row.
  • A zero-bed hospital receives the full warning class.
  • The initial render function fills the case queue plus hospital table when the page loads.

Before you refresh, how many case buttons plus hospital rows do you expect the two arrays to produce?

  • Save app.js.
  • Switch back to the Microsoft Edge page.
  • Refresh the page.

You'll see three synthetic cases plus four fictional hospitals. The educational-simulation warning remains visible above them.

  • Select ER-105 in the case queue.

You'll see the yellow ER-105 button become active. That's the first live board complete: its queue responds while its capacity table shows the network state.

Is the queue or table empty?

Open the Microsoft Edge Console to check for a syntax error. Compare each selector in app.js with the matching identifier in index.html.

Ask for help: Help me debug the empty queue or hospital table.

✔️ Awesome, I've got everything!

Your three project files are saved. The operations board now displays its synthetic queue plus fictional network capacity.

ⓧ I'd like to double check the full code

Compare each saved file with these complete references.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Emergency Network Control Centre</title>
  <link rel="stylesheet" href="styles.css">
  <script src="app.js" defer></script>
</head>
<body>
  <header class="topbar">
    <div>
      <p class="eyebrow">Hospital Network Operations</p>
      <h1>Emergency Control Centre</h1>
    </div>
    <span class="simulation-badge">Educational simulation | Synthetic data</span>
  </header>

  <main class="dashboard">
    <section class="panel span-2 notice">
      <strong>Not for clinical use.</strong>
      This prototype displays fictional upstream AI suggestions and a project-defined routing heuristic. A human operator makes the final simulated decision.
    </section>

    <section class="panel">
      <div class="section-heading">
        <div>
          <p class="eyebrow">Incoming</p>
          <h2>Case queue</h2>
        </div>
      </div>
      <div id="case-list" class="case-list"></div>
    </section>

    <section class="panel">
      <div class="section-heading">
        <div>
          <p class="eyebrow">Review</p>
          <h2>Selected case</h2>
        </div>
      </div>
      <div id="selected-case"></div>

      <div class="route-grid">
        <article class="route-card">
          <h3>Naive nearest route</h3>
          <div id="naive-route"></div>
        </article>
        <article class="route-card recommended">
          <h3>Network-aware route</h3>
          <div id="smart-route"></div>
        </article>
      </div>

      <div class="operator-controls">
        <button id="approve-route" type="button">Approve recommendation</button>
        <select id="override-hospital" aria-label="Override destination"></select>
        <button id="override-route" class="secondary" type="button">Override route</button>
      </div>
    </section>

    <section class="panel span-2">
      <div class="section-heading">
        <div>
          <p class="eyebrow">Live network</p>
          <h2>Hospital capacity</h2>
        </div>
      </div>
      <div class="table-wrap">
        <table>
          <thead>
            <tr>
              <th>Hospital</th>
              <th>Travel</th>
              <th>Available beds</th>
              <th>Services</th>
            </tr>
          </thead>
          <tbody id="hospital-body"></tbody>
        </table>
      </div>
    </section>

    <section class="panel span-2">
      <div class="section-heading">
        <div>
          <p class="eyebrow">Traceability</p>
          <h2>Decision audit log</h2>
        </div>
      </div>
      <ul id="decision-log" class="decision-log"></ul>
    </section>
  </main>

  <footer>
    Fictional hospitals, travel times, cases, services, and capacities are used throughout.
  </footer>
</body>
</html>

What does this HTML reference include?

This is the complete dashboard structure for the project. Its empty route plus decision containers are ready for later steps.

:root {
  color-scheme: light;
  font-family: Inter, "Segoe UI", Arial, sans-serif;
  background: #eef2f7;
  color: #172033;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
}

button,
select {
  font: inherit;
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.25rem clamp(1rem, 4vw, 3rem);
  color: white;
  background: #10243e;
}

h1,
h2,
h3,
p {
  margin-top: 0;
}

h1 {
  margin-bottom: 0;
  font-size: clamp(1.5rem, 4vw, 2.25rem);
}

h2 {
  margin-bottom: 0;
  font-size: 1.1rem;
}

h3 {
  font-size: 0.95rem;
}

.eyebrow {
  margin-bottom: 0.3rem;
  color: #6b7a90;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.topbar .eyebrow {
  color: #9fc3eb;
}

.simulation-badge {
  padding: 0.55rem 0.8rem;
  border: 1px solid #4d6f94;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 700;
}

.dashboard {
  display: grid;
  grid-template-columns: minmax(260px, 0.8fr) minmax(420px, 1.6fr);
  gap: 1rem;
  width: min(1200px, calc(100% - 2rem));
  margin: 1rem auto;
}

.panel {
  padding: 1rem;
  border: 1px solid #dce3ec;
  border-radius: 14px;
  background: white;
  box-shadow: 0 8px 24px rgba(16, 36, 62, 0.06);
}

.span-2 {
  grid-column: 1 / -1;
}

.notice {
  border-left: 5px solid #e2a100;
  background: #fff9e9;
}

.section-heading {
  display: flex;
  justify-content: space-between;
  margin-bottom: 0.8rem;
}

.case-list {
  display: grid;
  gap: 0.7rem;
}

.case-button {
  width: 100%;
  padding: 0.8rem;
  border: 1px solid #dce3ec;
  border-radius: 10px;
  color: inherit;
  background: white;
  text-align: left;
  cursor: pointer;
}

.case-button:hover,
.case-button.active {
  border-color: #2d69a5;
  background: #f2f7fc;
}

.case-topline,
.selected-topline {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.case-meta,
.muted {
  color: #627087;
  font-size: 0.82rem;
}

.triage {
  display: inline-block;
  padding: 0.25rem 0.55rem;
  border-radius: 999px;
  color: white;
  font-size: 0.7rem;
  font-weight: 800;
}

.triage.red {
  background: #c62828;
}

.triage.yellow {
  color: #3d2c00;
  background: #f6c344;
}

.triage.green {
  background: #17824b;
}

.route-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.8rem;
  margin-top: 1rem;
}

.route-card {
  min-height: 150px;
  padding: 0.9rem;
  border: 1px solid #dce3ec;
  border-radius: 10px;
  background: #f8fafc;
}

.route-card.recommended {
  border-color: #8ab6df;
  background: #eff7ff;
}

.warning {
  color: #a43b22;
  font-weight: 700;
}

.success {
  color: #176b40;
  font-weight: 700;
}

.operator-controls {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 0.6rem;
  margin-top: 1rem;
}

button,
select {
  min-height: 42px;
  padding: 0.65rem 0.75rem;
  border: 1px solid #2d69a5;
  border-radius: 8px;
}

button {
  color: white;
  background: #2d69a5;
  font-weight: 750;
  cursor: pointer;
}

button.secondary {
  color: #2d69a5;
  background: white;
}

button:disabled,
select:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

.table-wrap {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
}

th,
td {
  padding: 0.75rem;
  border-bottom: 1px solid #e4e9f0;
  text-align: left;
}

th {
  color: #627087;
  font-size: 0.76rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.full {
  color: #b3261e;
  font-weight: 800;
}

.decision-log {
  display: grid;
  gap: 0.6rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.decision-log li {
  padding: 0.75rem;
  border-left: 4px solid #2d69a5;
  border-radius: 6px;
  background: #f5f8fc;
}

footer {
  padding: 0.5rem 1rem 1.5rem;
  color: #627087;
  font-size: 0.75rem;
  text-align: center;
}

@media (max-width: 800px) {
  .topbar,
  .case-topline,
  .selected-topline {
    align-items: flex-start;
    flex-direction: column;
  }

  .dashboard,
  .route-grid,
  .operator-controls {
    grid-template-columns: 1fr;
  }

  .span-2 {
    grid-column: auto;
  }
}

What does this stylesheet reference include?

This is the complete responsive stylesheet. It includes every panel plus badge state needed by the later routing steps.

const hospitals = [
  {
    id: "central",
    name: "Central General",
    travelMinutes: 7,
    beds: 0,
    services: ["General", "Cardiac"],
  },
  {
    id: "riverside",
    name: "Riverside Trauma",
    travelMinutes: 12,
    beds: 1,
    services: ["Trauma", "General"],
  },
  {
    id: "north-heart",
    name: "North Heart Institute",
    travelMinutes: 18,
    beds: 1,
    services: ["Cardiac"],
  },
  {
    id: "lakeside",
    name: "Lakeside Medical",
    travelMinutes: 14,
    beds: 1,
    services: ["General"],
  },
];

const cases = [
  {
    id: "ER-104",
    ageGroup: "Adult",
    triage: "Red",
    summary: "Synthetic high-acuity trauma scenario",
    requiredService: "Trauma",
    aiReason: "Upstream simulation flagged immediate review.",
    status: "Pending",
  },
  {
    id: "ER-105",
    ageGroup: "Adult",
    triage: "Yellow",
    summary: "Synthetic moderate-acuity cardiac scenario",
    requiredService: "Cardiac",
    aiReason: "Upstream simulation flagged review soon.",
    status: "Pending",
  },
  {
    id: "ER-106",
    ageGroup: "Adult",
    triage: "Green",
    summary: "Synthetic low-acuity general-care scenario",
    requiredService: "General",
    aiReason: "Upstream simulation marked the case able to wait.",
    status: "Pending",
  },
];

let selectedCaseId = "ER-104";
const decisionLog = [];

const caseList = document.querySelector("#case-list");
const selectedCasePanel = document.querySelector("#selected-case");
const naiveRoutePanel = document.querySelector("#naive-route");
const smartRoutePanel = document.querySelector("#smart-route");
const hospitalBody = document.querySelector("#hospital-body");
const decisionLogPanel = document.querySelector("#decision-log");
const approveRouteButton = document.querySelector("#approve-route");
const overrideHospitalSelect = document.querySelector("#override-hospital");
const overrideRouteButton = document.querySelector("#override-route");

function getSelectedCase() {
  return cases.find((patientCase) => patientCase.id === selectedCaseId);
}

function renderCaseList() {
  caseList.innerHTML = cases
    .map(
      (patientCase) => `
        <button
          class="case-button ${patientCase.id === selectedCaseId ? "active" : ""}"
          data-case-id="${patientCase.id}"
          type="button"
        >
          <span class="case-topline">
            <strong>${patientCase.id}</strong>
            <span class="triage ${patientCase.triage.toLowerCase()}">${patientCase.triage}</span>
          </span>
          <span class="case-meta">${patientCase.requiredService} | ${patientCase.status}</span>
        </button>
      `,
    )
    .join("");

  document.querySelectorAll("[data-case-id]").forEach((button) => {
    button.addEventListener("click", () => {
      selectedCaseId = button.dataset.caseId;
      renderAll();
    });
  });
}

function renderHospitals() {
  hospitalBody.innerHTML = hospitals
    .map(
      (hospital) => `
        <tr>
          <td><strong>${hospital.name}</strong></td>
          <td>${hospital.travelMinutes} min</td>
          <td class="${hospital.beds === 0 ? "full" : ""}">${hospital.beds}</td>
          <td>${hospital.services.join(", ")}</td>
        </tr>
      `,
    )
    .join("");
}

function renderAll() {
  renderCaseList();
  renderHospitals();
}

renderAll();

What does this script reference include?

This is the complete script for the current step. It defines the synthetic data plus the two active renderers.

Your Control Centre now exposes the incoming cases plus current capacity. Next up, you will see why choosing only the nearest hospital can produce a flawed route.

Expose the Nearest-Hospital Failure

Your operations board now shows every synthetic case beside the hospital network's current capacity. The operator can see the situation before making a routing decision.

A nearest-hospital rule looks efficient. However, it ignores whether the destination has an available bed or the required service. In this step, you'll let that limited rule make its choice before the dashboard tests what it overlooked.

In this step, get ready to:
  • Create a nearest route based only on fictional travel time.
  • Check the selected hospital for capacity and service conflicts.
  • Render the selected case and expose the routing failure for ER-104.
Select the shortest travel time

The first helper uses JavaScript array sorting to rank hospitals by travel time. It deliberately leaves beds and services out of the decision.

  • Return to app.js in Visual Studio Code.
  • Find the existing getSelectedCase() function.
  • Add naiveRoute() directly below it by copying this code:
function naiveRoute() {
  return [...hospitals].sort(
    (first, second) => first.travelMinutes - second.travelMinutes,
  )[0];
}

What does this function do?

  • The spread syntax copies hospitals before sorting. This preserves the original hospital order.
  • The comparison function orders hospitals from the lowest travelMinutes value to the highest value.
  • The [0] lookup returns the hospital at the front of the sorted copy.
  • Save app.js.
  • Switch back to Microsoft Edge.
  • Refresh the local index.html page.

You should still see three cases. The hospital table should still show four hospitals.

Did the dashboard disappear?

  • Check that naiveRoute() has one opening brace and one closing brace.
  • Check the comma after the comparison function. It must remain inside the sort() call.

Ask for help: Why did my dashboard stop rendering after I added naiveRoute()?

Check capacity and service constraints

A route can fail for more than one reason. The next helper collects every issue so the operator receives a complete explanation.

  • Find the new naiveRoute() function in app.js.
  • Add getRouteIssues() directly below it by copying this code:
function getRouteIssues(hospital, patientCase) {
  const issues = [];

  if (hospital.beds <= 0) {
    issues.push("no available beds");
  }

  if (!hospital.services.includes(patientCase.requiredService)) {
    issues.push(`no ${patientCase.requiredService} service`);
  }

  return issues;
}

How are route issues collected?

  • The issues array starts empty. Each failed constraint adds a plain-language reason.
  • The bed check catches hospitals with zero available beds.
  • The service check compares the selected case's requiredService with the hospital's services list.
  • Returning the complete array lets the dashboard show both failures when both constraints fail.
  • Save app.js.
  • Switch back to Microsoft Edge.
  • Refresh the local page.

You should still see the three case buttons. You should also see the unchanged hospital-capacity table.

Did the case queue stop loading?

  • Check that issues is declared inside getRouteIssues().
  • Check the spelling of patientCase.requiredService against the code above.
  • Check that both text messages use the same quotation marks shown in the code.

Ask for help: Can you help me find the syntax error in getRouteIssues()?

Render the routing failure

The selected-case panel connects the route calculation to something the operator can inspect. It displays the case details first. It then shows the nearest hospital beside every failed constraint.

  • Find getRouteIssues() in app.js.
  • Add renderSelectedCase() directly below it by copying this code:
function renderSelectedCase() {
  const patientCase = getSelectedCase();
  const nearest = naiveRoute();
  const nearestIssues = getRouteIssues(nearest, patientCase);

  selectedCasePanel.innerHTML = `
    <div class="selected-topline">
      <div>
        <strong>${patientCase.id}</strong>
        <p class="muted">${patientCase.ageGroup} | ${patientCase.requiredService}</p>
      </div>
      <span class="triage ${patientCase.triage.toLowerCase()}">${patientCase.triage}</span>
    </div>
    <p>${patientCase.summary}</p>
    <p class="muted">${patientCase.aiReason}</p>
    <p><strong>Status:</strong> ${patientCase.status}</p>
  `;

  naiveRoutePanel.innerHTML = `
    <p><strong>${nearest.name}</strong> (${nearest.travelMinutes} min)</p>
    <p class="${nearestIssues.length > 0 ? "warning" : "success"}">
      ${
        nearestIssues.length > 0
          ? `Fails because: ${nearestIssues.join(" and ")}.`
          : "Works by chance, but ignores network constraints."
      }
    </p>
  `;
}

What does this rendering function show?

  • The first three constants retrieve the selected case. They also calculate its nearest hospital and collect the route issues.
  • The selected-case template displays the case identifier, required service, triage level, summary, upstream suggestion, and status.
  • The naive-route template displays the nearest hospital's name and fictional travel time.
  • The warning joins multiple route issues with and. The operator can see every failed constraint in one result.
  • Scroll to the existing renderAll() function near the bottom of app.js.
  • Find this current version:
function renderAll() {
  renderCaseList();
  renderHospitals();
}

What does the current render cycle cover?

The current function refreshes the case queue and hospital table. The selected-case renderer needs a place in this same cycle.

  • Replace the current renderAll() function with this updated version:
function renderAll() {
  renderCaseList();
  renderSelectedCase();
  renderHospitals();
}

Why add the selected-case renderer here?

Every case selection already calls renderAll(). Adding renderSelectedCase() keeps the case details and routing result synchronized with the active case.

  • Save app.js.

Before you refresh, which hospital do you think the shortest-travel-time rule chooses for ER-104?

  • Switch back to Microsoft Edge.
  • Refresh the local index.html page.
  • Select ER-104 from the case queue.

You'll see Central General as the nearest result at 7 min. The warning reports Fails because: no available beds and no Trauma service.

You've exposed the intended shortfall. The fastest destination cannot accept this trauma case under the fictional network constraints.

Missing the selected-case details?

  • Check that renderSelectedCase() appears inside renderAll().
  • Check that the function name uses the same capital letters in its definition and call.
  • Check that selectedCasePanel and naiveRoutePanel match the existing DOM constants.

Ask for help: Why is my selected case or naive route missing after refresh?

✔️ Awesome, I've got everything!

Great. Double check that app.js is saved. Keep ER-104 selected so both failure reasons remain visible.

ⓧ I'd like to double check the full code

Compare your cumulative app.js file with this version.

const hospitals = [
  {
    id: "central",
    name: "Central General",
    travelMinutes: 7,
    beds: 0,
    services: ["General", "Cardiac"],
  },
  {
    id: "riverside",
    name: "Riverside Trauma",
    travelMinutes: 12,
    beds: 1,
    services: ["Trauma", "General"],
  },
  {
    id: "north-heart",
    name: "North Heart Institute",
    travelMinutes: 18,
    beds: 1,
    services: ["Cardiac"],
  },
  {
    id: "lakeside",
    name: "Lakeside Medical",
    travelMinutes: 14,
    beds: 1,
    services: ["General"],
  },
];

const cases = [
  {
    id: "ER-104",
    ageGroup: "Adult",
    triage: "Red",
    summary: "Synthetic high-acuity trauma scenario",
    requiredService: "Trauma",
    aiReason: "Upstream simulation flagged immediate review.",
    status: "Pending",
  },
  {
    id: "ER-105",
    ageGroup: "Adult",
    triage: "Yellow",
    summary: "Synthetic moderate-acuity cardiac scenario",
    requiredService: "Cardiac",
    aiReason: "Upstream simulation flagged review soon.",
    status: "Pending",
  },
  {
    id: "ER-106",
    ageGroup: "Adult",
    triage: "Green",
    summary: "Synthetic low-acuity general-care scenario",
    requiredService: "General",
    aiReason: "Upstream simulation marked the case able to wait.",
    status: "Pending",
  },
];

let selectedCaseId = "ER-104";
const decisionLog = [];

const caseList = document.querySelector("#case-list");
const selectedCasePanel = document.querySelector("#selected-case");
const naiveRoutePanel = document.querySelector("#naive-route");
const smartRoutePanel = document.querySelector("#smart-route");
const hospitalBody = document.querySelector("#hospital-body");
const decisionLogPanel = document.querySelector("#decision-log");
const approveRouteButton = document.querySelector("#approve-route");
const overrideHospitalSelect = document.querySelector("#override-hospital");
const overrideRouteButton = document.querySelector("#override-route");

function getSelectedCase() {
  return cases.find((patientCase) => patientCase.id === selectedCaseId);
}

function naiveRoute() {
  return [...hospitals].sort(
    (first, second) => first.travelMinutes - second.travelMinutes,
  )[0];
}

function getRouteIssues(hospital, patientCase) {
  const issues = [];

  if (hospital.beds <= 0) {
    issues.push("no available beds");
  }

  if (!hospital.services.includes(patientCase.requiredService)) {
    issues.push(`no ${patientCase.requiredService} service`);
  }

  return issues;
}

function renderCaseList() {
  caseList.innerHTML = cases
    .map(
      (patientCase) => `
        <button
          class="case-button ${patientCase.id === selectedCaseId ? "active" : ""}"
          data-case-id="${patientCase.id}"
          type="button"
        >
          <span class="case-topline">
            <strong>${patientCase.id}</strong>
            <span class="triage ${patientCase.triage.toLowerCase()}">${patientCase.triage}</span>
          </span>
          <span class="case-meta">${patientCase.requiredService} | ${patientCase.status}</span>
        </button>
      `,
    )
    .join("");

  document.querySelectorAll("[data-case-id]").forEach((button) => {
    button.addEventListener("click", () => {
      selectedCaseId = button.dataset.caseId;
      renderAll();
    });
  });
}

function renderSelectedCase() {
  const patientCase = getSelectedCase();
  const nearest = naiveRoute();
  const nearestIssues = getRouteIssues(nearest, patientCase);

  selectedCasePanel.innerHTML = `
    <div class="selected-topline">
      <div>
        <strong>${patientCase.id}</strong>
        <p class="muted">${patientCase.ageGroup} | ${patientCase.requiredService}</p>
      </div>
      <span class="triage ${patientCase.triage.toLowerCase()}">${patientCase.triage}</span>
    </div>
    <p>${patientCase.summary}</p>
    <p class="muted">${patientCase.aiReason}</p>
    <p><strong>Status:</strong> ${patientCase.status}</p>
  `;

  naiveRoutePanel.innerHTML = `
    <p><strong>${nearest.name}</strong> (${nearest.travelMinutes} min)</p>
    <p class="${nearestIssues.length > 0 ? "warning" : "success"}">
      ${
        nearestIssues.length > 0
          ? `Fails because: ${nearestIssues.join(" and ")}.`
          : "Works by chance, but ignores network constraints."
      }
    </p>
  `;
}

function renderHospitals() {
  hospitalBody.innerHTML = hospitals
    .map(
      (hospital) => `
        <tr>
          <td><strong>${hospital.name}</strong></td>
          <td>${hospital.travelMinutes} min</td>
          <td class="${hospital.beds === 0 ? "full" : ""}">${hospital.beds}</td>
          <td>${hospital.services.join(", ")}</td>
        </tr>
      `,
    )
    .join("");
}

function renderAll() {
  renderCaseList();
  renderSelectedCase();
  renderHospitals();
}

renderAll();

This reference includes the synthetic data and rendering code from the previous step. It also includes the three routing additions from this step.

Your console now proves that distance alone can produce an unusable destination. Next up, you'll filter out impossible hospitals before ranking the eligible options.

Add Explainable Network-Aware Routing

Your dashboard now exposes the weakness in nearest-only routing. Central General is fastest. It still has no available bed or Trauma service.

This step adds constraint-aware routing that filters impossible destinations first. A transparent simulation score then ranks the eligible hospitals.

In this step, get ready to:
  • Filter out hospitals that cannot accept the selected case.
  • Rank eligible hospitals with the project-defined simulation score.
  • Render an explainable recommendation in the network-aware panel.
Filter and score eligible hospitals

An eligible destination needs an available bed. It also needs the service required by the selected case.

The simulation score combines travel time with a capacity penalty. Hospitals with fewer remaining beds receive a larger penalty.

  • Switch back to the app.js tab in Visual Studio Code.
  • Find the closing brace of getRouteIssues(hospital, patientCase).
  • Add the following functions directly below it:
function capacityPenalty(hospital) {
  return Math.max(0, 5 - hospital.beds) * 3;
}

function recommendRoute(patientCase) {
  const eligibleHospitals = hospitals.filter(
    (hospital) =>
      hospital.beds > 0 &&
      hospital.services.includes(patientCase.requiredService),
  );

  if (eligibleHospitals.length === 0) {
    return null;
  }

  return eligibleHospitals
    .map((hospital) => ({
      hospital,
      penalty: capacityPenalty(hospital),
      score: hospital.travelMinutes + capacityPenalty(hospital),
    }))
    .sort((first, second) => first.score - second.score)[0];
}

How does the routing score work?

  • The capacityPenalty() function turns low capacity into a larger score contribution.
  • The filter() step keeps hospitals with at least one bed.
  • The same filter requires the service named by patientCase.requiredService.
  • The sort() step places the lowest total score first.
  • The function returns null when no hospital clears both constraints.

Why filter before scoring?

Filtering prevents an impossible destination from winning because of a short travel time. Scoring then compares only the hospitals that can accept the simulated case.

This score is a project-defined educational heuristic. Its value comes from making each ranking factor visible to the operator.

  • Save app.js.
  • Switch back to Microsoft Edge.
  • Refresh the local index.html page.

Good. The dashboard still shows all three cases plus the Central General failure for ER-104.

Did the dashboard stop loading?

  • Check that capacityPenalty() starts after the closing brace of getRouteIssues().
  • Compare every parenthesis in eligibleHospitals with the code above.
  • Check that patientCase.requiredService uses the same capitalization in both places.

Ask for help: Help me debug the new routing functions in app.js.

Render the explainable recommendation

The recommendation object contains the chosen hospital. It also carries both score components.

The network-aware panel turns those values into evidence an operator can review before making a simulated decision.

  • Find the const nearestIssues = getRouteIssues(nearest, patientCase); line inside renderSelectedCase().
  • Add the following line directly below it:
  const recommendation = recommendRoute(patientCase);

Why store the recommendation?

This line runs the new routing function for the selected case. The renderer can now use one result for the destination plus its score breakdown.

  • Locate the end of the naiveRoutePanel.innerHTML assignment inside renderSelectedCase().
  • Add the following block below that assignment:
  if (recommendation) {
    smartRoutePanel.innerHTML = `
      <p><strong>${recommendation.hospital.name}</strong></p>
      <p>${recommendation.hospital.travelMinutes} travel minutes + ${recommendation.penalty} capacity penalty = <strong>${recommendation.score}</strong></p>
      <p class="success">Eligible: available bed and matching ${patientCase.requiredService} service.</p>
      <p class="muted">Lower simulation score ranks first. This is not a clinical rule.</p>
    `;
  } else {
    smartRoutePanel.innerHTML = `
      <p class="warning">No eligible destination.</p>
      <p class="muted">No hospital has both an available bed and the required ${patientCase.requiredService} service. Escalation is required in this simulation.</p>
    `;
  }

What does this panel show?

  • The first branch displays the eligible hospital.
  • The score line exposes each contribution to the total.
  • The success line explains which constraints the hospital passed.
  • The fallback requires escalation when no hospital qualifies.
  • The final note identifies the score as an educational simulation heuristic.
  • Save app.js.

Before you refresh, which hospital do you expect to pass the bed and Trauma-service filters?

  • Switch back to Microsoft Edge.
  • Refresh the local index.html page.
  • Select ER-104 from the case queue.

You’ll see Riverside Trauma as the recommendation. The score line reads 12 travel minutes + 12 capacity penalty = 24.

The panel confirms an available bed plus a matching Trauma service. It also states that the score is not a clinical rule.

You have closed the routing gap. The recommendation now exposes enough evidence for an operator to review its result.

Missing the network-aware result?

  • Check that const recommendation = recommendRoute(patientCase); appears near the start of renderSelectedCase().
  • Check that the recommendation block remains inside renderSelectedCase().
  • Confirm that smartRoutePanel matches the existing reference to #smart-route.

Ask for help: Help me find why the network-aware route is missing from my dashboard.

✔️ Awesome, I've got everything

Great. Double check that app.js is saved before moving on.

ⓧ I'd like to double check the full code

Compare your cumulative app.js file with this version.

const hospitals = [
  {
    id: "central",
    name: "Central General",
    travelMinutes: 7,
    beds: 0,
    services: ["General", "Cardiac"],
  },
  {
    id: "riverside",
    name: "Riverside Trauma",
    travelMinutes: 12,
    beds: 1,
    services: ["Trauma", "General"],
  },
  {
    id: "north-heart",
    name: "North Heart Institute",
    travelMinutes: 18,
    beds: 1,
    services: ["Cardiac"],
  },
  {
    id: "lakeside",
    name: "Lakeside Medical",
    travelMinutes: 14,
    beds: 1,
    services: ["General"],
  },
];

const cases = [
  {
    id: "ER-104",
    ageGroup: "Adult",
    triage: "Red",
    summary: "Synthetic high-acuity trauma scenario",
    requiredService: "Trauma",
    aiReason: "Upstream simulation flagged immediate review.",
    status: "Pending",
  },
  {
    id: "ER-105",
    ageGroup: "Adult",
    triage: "Yellow",
    summary: "Synthetic moderate-acuity cardiac scenario",
    requiredService: "Cardiac",
    aiReason: "Upstream simulation flagged review soon.",
    status: "Pending",
  },
  {
    id: "ER-106",
    ageGroup: "Adult",
    triage: "Green",
    summary: "Synthetic low-acuity general-care scenario",
    requiredService: "General",
    aiReason: "Upstream simulation marked the case able to wait.",
    status: "Pending",
  },
];

let selectedCaseId = "ER-104";

const caseList = document.querySelector("#case-list");
const selectedCasePanel = document.querySelector("#selected-case");
const naiveRoutePanel = document.querySelector("#naive-route");
const smartRoutePanel = document.querySelector("#smart-route");
const hospitalBody = document.querySelector("#hospital-body");

function getSelectedCase() {
  return cases.find((patientCase) => patientCase.id === selectedCaseId);
}

function naiveRoute() {
  return [...hospitals].sort(
    (first, second) => first.travelMinutes - second.travelMinutes,
  )[0];
}

function getRouteIssues(hospital, patientCase) {
  const issues = [];

  if (hospital.beds <= 0) {
    issues.push("no available beds");
  }

  if (!hospital.services.includes(patientCase.requiredService)) {
    issues.push(`no ${patientCase.requiredService} service`);
  }

  return issues;
}

function capacityPenalty(hospital) {
  return Math.max(0, 5 - hospital.beds) * 3;
}

function recommendRoute(patientCase) {
  const eligibleHospitals = hospitals.filter(
    (hospital) =>
      hospital.beds > 0 &&
      hospital.services.includes(patientCase.requiredService),
  );

  if (eligibleHospitals.length === 0) {
    return null;
  }

  return eligibleHospitals
    .map((hospital) => ({
      hospital,
      penalty: capacityPenalty(hospital),
      score: hospital.travelMinutes + capacityPenalty(hospital),
    }))
    .sort((first, second) => first.score - second.score)[0];
}

function renderCaseList() {
  caseList.innerHTML = cases
    .map(
      (patientCase) => `
        <button
          class="case-button ${patientCase.id === selectedCaseId ? "active" : ""}"
          data-case-id="${patientCase.id}"
          type="button"
        >
          <span class="case-topline">
            <strong>${patientCase.id}</strong>
            <span class="triage ${patientCase.triage.toLowerCase()}">${patientCase.triage}</span>
          </span>
          <span class="case-meta">${patientCase.requiredService} | ${patientCase.status}</span>
        </button>
      `,
    )
    .join("");

  document.querySelectorAll("[data-case-id]").forEach((button) => {
    button.addEventListener("click", () => {
      selectedCaseId = button.dataset.caseId;
      renderAll();
    });
  });
}

function renderSelectedCase() {
  const patientCase = getSelectedCase();
  const nearest = naiveRoute();
  const nearestIssues = getRouteIssues(nearest, patientCase);
  const recommendation = recommendRoute(patientCase);

  selectedCasePanel.innerHTML = `
    <div class="selected-topline">
      <div>
        <strong>${patientCase.id}</strong>
        <p class="muted">${patientCase.ageGroup} | ${patientCase.requiredService}</p>
      </div>
      <span class="triage ${patientCase.triage.toLowerCase()}">${patientCase.triage}</span>
    </div>
    <p>${patientCase.summary}</p>
    <p class="muted">${patientCase.aiReason}</p>
    <p><strong>Status:</strong> ${patientCase.status}</p>
  `;

  naiveRoutePanel.innerHTML = `
    <p><strong>${nearest.name}</strong> (${nearest.travelMinutes} min)</p>
    <p class="${nearestIssues.length > 0 ? "warning" : "success"}">
      ${
        nearestIssues.length > 0
          ? `Fails because: ${nearestIssues.join(" and ")}.`
          : "Works by chance, but ignores network constraints."
      }
    </p>
  `;

  if (recommendation) {
    smartRoutePanel.innerHTML = `
      <p><strong>${recommendation.hospital.name}</strong></p>
      <p>${recommendation.hospital.travelMinutes} travel minutes + ${recommendation.penalty} capacity penalty = <strong>${recommendation.score}</strong></p>
      <p class="success">Eligible: available bed and matching ${patientCase.requiredService} service.</p>
      <p class="muted">Lower simulation score ranks first. This is not a clinical rule.</p>
    `;
  } else {
    smartRoutePanel.innerHTML = `
      <p class="warning">No eligible destination.</p>
      <p class="muted">No hospital has both an available bed and the required ${patientCase.requiredService} service. Escalation is required in this simulation.</p>
    `;
  }
}

function renderHospitals() {
  hospitalBody.innerHTML = hospitals
    .map(
      (hospital) => `
        <tr>
          <td><strong>${hospital.name}</strong></td>
          <td>${hospital.travelMinutes} min</td>
          <td class="${hospital.beds === 0 ? "full" : ""}">${hospital.beds}</td>
          <td>${hospital.services.join(", ")}</td>
        </tr>
      `,
    )
    .join("");
}

function renderAll() {
  renderCaseList();
  renderSelectedCase();
  renderHospitals();
}

renderAll();

Your console now turns live constraints into a transparent routing recommendation. Next, you’ll give the operator approval controls plus a traceable audit log.

Add Human Approval and an Audit Log

Your network-aware panel now recommends a destination after checking capacity and service requirements. The operator can review the reason behind that recommendation.

Automated recommendations still need human oversight. This step adds approval controls plus an audit log that makes every simulated decision traceable.

In this step, get ready to:
  • Populate the override selector with hospitals that have capacity.
  • Update each dispatched case plus its destination hospital.
  • Record every approval or override in the decision audit log.
Connect the operator controls

The controls already exist in index.html. Your JavaScript needs references to those elements before it can populate the selector or respond to the buttons.

  • In app.js, find the hospitalBody declaration.
  • Connect the audit log plus the three operator controls by adding this code directly below that declaration:
const decisionLogPanel = document.querySelector("#decision-log");
const approveRouteButton = document.querySelector("#approve-route");
const overrideHospitalSelect = document.querySelector("#override-hospital");
const overrideRouteButton = document.querySelector("#override-route");

What do these references do?

  • The decisionLogPanel reference gives the script a place to display recorded decisions.
  • The remaining references connect the approval button plus the override selector plus the override button to their existing HTML elements.
  • Save app.js.
  • Switch back to Microsoft Edge.
  • Refresh index.html.

You should still see the routing panels plus all three operator controls. This confirms that the new element references load without interrupting the dashboard.

  • In renderSelectedCase(), find the line containing const recommendation = recommendRoute(patientCase);.
  • Track whether the selected case can still be dispatched by adding this line directly below it:
  const isPending = patientCase.status === "Pending";

Why track pending status?

A case should accept one simulated decision. The isPending value lets the interface disable its controls after dispatch.

  • At the end of renderSelectedCase(), find the closing brace that appears after the network-aware route rendering.
  • Populate the override selector by adding this code immediately before that closing brace:
  const hospitalsWithCapacity = hospitals.filter((hospital) => hospital.beds > 0);

  overrideHospitalSelect.innerHTML = hospitalsWithCapacity
    .map(
      (hospital) => `
        <option
          value="${hospital.id}"
          ${recommendation?.hospital.id === hospital.id ? "selected" : ""}
        >
          ${hospital.name} | ${hospital.services.join("/")}
        </option>
      `,
    )
    .join("");

  approveRouteButton.disabled = !recommendation || !isPending;
  overrideHospitalSelect.disabled = hospitalsWithCapacity.length === 0 || !isPending;
  overrideRouteButton.disabled = hospitalsWithCapacity.length === 0 || !isPending;

How do the controls reflect network state?

  • The selector includes every hospital whose beds value remains above zero.
  • The current recommendation becomes the selected override option when it remains available.
  • The controls become unavailable when the selected case has already been dispatched.
  • Save app.js.
  • Refresh index.html in Microsoft Edge.

You should see Central General excluded from the override selector because it has zero beds. Riverside Trauma should be selected for ER-104.

Good progress. The controls now reflect the live capacity data while protecting cases that have already received a decision.

Selector empty or controls disabled?

  • Check that hospitalsWithCapacity filters with hospital.beds > 0.
  • Check that isPending compares the case status with Pending using matching capitalization.
  • Confirm that the new selector code sits inside renderSelectedCase().

Ask for help: Help me debug why my override selector is empty or disabled for ER-104.

Record decisions in the audit log

An empty log needs a clear starting state. Once decisions exist, the newest record should appear first so the operator sees the latest network change immediately.

  • In app.js, locate the closing brace of renderHospitals().
  • Render the decision history by adding this function directly below renderHospitals().
function renderDecisionLog() {
  if (decisionLog.length === 0) {
    decisionLogPanel.innerHTML = "<li>No decisions recorded yet.</li>";
    return;
  }

  decisionLogPanel.innerHTML = decisionLog
    .map(
      (entry) => `
        <li>
          <strong>${entry.caseId}</strong> routed to <strong>${entry.hospitalName}</strong>
          by ${entry.decisionType} at ${entry.time}.
        </li>
      `,
    )
    .join("");
}

What does the audit renderer show?

An empty decisionLog displays No decisions recorded yet. as a visible baseline.

Each stored entry displays the case plus its destination. It also displays the decision type plus the local time.

  • At the bottom of app.js, replace the standalone render calls with this combined renderer:
function renderAll() {
  renderCaseList();
  renderSelectedCase();
  renderHospitals();
  renderDecisionLog();
}

renderAll();

Why use one renderer?

The renderAll() function refreshes every view that depends on case state or hospital capacity. One call keeps the queue plus routing panels plus table plus audit log synchronized.

  • Save app.js.
  • Refresh index.html in Microsoft Edge.

You should see No decisions recorded yet. inside the Decision audit log panel. The message proves that renderDecisionLog() now participates in the main render cycle.

  • In renderCaseList(), find the click handler containing selectedCaseId = button.dataset.caseId;.
  • Replace the refresh calls inside that handler with this exact pair of lines:
      selectedCaseId = button.dataset.caseId;
      renderAll();

What changes when a case is selected?

Selecting a case now refreshes every state-dependent panel through renderAll(). This keeps the selected case plus its routes plus its available controls aligned.

  • Save app.js.
  • Refresh index.html in Microsoft Edge.
  • Select ER-105 from the case queue.

You should see the selected-case details plus both routing panels update for ER-105. The audit log should continue to show its empty-state message.

Audit log still blank?

  • Check that renderAll() includes a call to renderDecisionLog().
  • Check that the final line of app.js calls renderAll().
  • Confirm that decisionLogPanel selects #decision-log.

Ask for help: Help me find why renderDecisionLog is not displaying its empty state.

Wire approval and verify the workflow

A committed decision changes three pieces of state. It consumes one fictional bed plus updates the case status plus prepends a traceable log entry.

  • In app.js, locate the closing brace of renderDecisionLog().
  • Commit valid decisions by adding this function directly below renderDecisionLog().
function commitDecision(decisionType, hospitalId) {
  const patientCase = getSelectedCase();
  const hospital = hospitals.find((item) => item.id === hospitalId);

  if (!hospital || hospital.beds <= 0 || patientCase.status !== "Pending") {
    return;
  }

  hospital.beds -= 1;
  patientCase.status = `Dispatched to ${hospital.name}`;

  decisionLog.unshift({
    caseId: patientCase.id,
    hospitalName: hospital.name,
    decisionType,
    time: new Date().toLocaleTimeString(),
  });

  renderAll();
}

How is a decision committed?

  • The guard clause stops a decision when the hospital is missing or full. It also stops repeat dispatches for a completed case.
  • A valid decision subtracts one from the destination's beds value.
  • The case status records its destination. The new log object records the case plus hospital plus decision type plus local time.
  • The final renderAll() call makes every change visible immediately.
  • Save app.js.
  • Refresh index.html in Microsoft Edge.

You should see ER-104 remain pending with Riverside Trauma at one bed. The audit log should still show its empty-state message.

  • In app.js, locate the closing brace of commitDecision().
  • Connect both decision buttons by adding these listeners directly below commitDecision().
approveRouteButton.addEventListener("click", () => {
  const recommendation = recommendRoute(getSelectedCase());

  if (recommendation) {
    commitDecision("operator approval", recommendation.hospital.id);
  }
});

overrideRouteButton.addEventListener("click", () => {
  commitDecision("operator override", overrideHospitalSelect.value);
});

How do approval and override differ?

Approval recalculates the current recommendation before committing its hospital. This prevents the button from relying on an outdated destination.

Override commits the hospital currently chosen by the operator. Both paths use the same state update plus audit process.

  • Save app.js.
  • Refresh index.html in Microsoft Edge.
  • Select ER-104 from the case queue.

Before you click, consider whether the case status plus Riverside Trauma capacity plus the audit log will change in the same render cycle.

  • Click Approve recommendation.

You should see ER-104 dispatched to Riverside Trauma. Riverside Trauma should drop from one bed to zero.

The newest audit entry should name ER-104 plus Riverside Trauma plus operator approval plus the local time. That is the complete human-reviewed workflow working across every panel.

Approval not updating the dashboard?

  • Check that the approval listener passes recommendation.hospital.id into commitDecision().
  • Check that commitDecision() calls renderAll() after adding the log entry.
  • Refresh the page to reset the synthetic cases if ER-104 was already dispatched during testing.

Ask for help: Help me debug why approving ER-104 does not update capacity or the audit log.

✔️ Awesome, I've got everything!

Your completed workflow now supports approval plus override plus capacity updates plus timestamped audit records. Double check that app.js is saved.

ⓧ I'd like to double check the full code

Compare your complete app.js file with this final version.

const hospitals = [
  {
    id: "central",
    name: "Central General",
    travelMinutes: 7,
    beds: 0,
    services: ["General", "Cardiac"],
  },
  {
    id: "riverside",
    name: "Riverside Trauma",
    travelMinutes: 12,
    beds: 1,
    services: ["Trauma", "General"],
  },
  {
    id: "north-heart",
    name: "North Heart Institute",
    travelMinutes: 18,
    beds: 1,
    services: ["Cardiac"],
  },
  {
    id: "lakeside",
    name: "Lakeside Medical",
    travelMinutes: 14,
    beds: 1,
    services: ["General"],
  },
];

const cases = [
  {
    id: "ER-104",
    ageGroup: "Adult",
    triage: "Red",
    summary: "Synthetic high-acuity trauma scenario",
    requiredService: "Trauma",
    aiReason: "Upstream simulation flagged immediate review.",
    status: "Pending",
  },
  {
    id: "ER-105",
    ageGroup: "Adult",
    triage: "Yellow",
    summary: "Synthetic moderate-acuity cardiac scenario",
    requiredService: "Cardiac",
    aiReason: "Upstream simulation flagged review soon.",
    status: "Pending",
  },
  {
    id: "ER-106",
    ageGroup: "Adult",
    triage: "Green",
    summary: "Synthetic low-acuity general-care scenario",
    requiredService: "General",
    aiReason: "Upstream simulation marked the case able to wait.",
    status: "Pending",
  },
];

let selectedCaseId = "ER-104";
const decisionLog = [];

const caseList = document.querySelector("#case-list");
const selectedCasePanel = document.querySelector("#selected-case");
const naiveRoutePanel = document.querySelector("#naive-route");
const smartRoutePanel = document.querySelector("#smart-route");
const hospitalBody = document.querySelector("#hospital-body");
const decisionLogPanel = document.querySelector("#decision-log");
const approveRouteButton = document.querySelector("#approve-route");
const overrideHospitalSelect = document.querySelector("#override-hospital");
const overrideRouteButton = document.querySelector("#override-route");

function getSelectedCase() {
  return cases.find((patientCase) => patientCase.id === selectedCaseId);
}

function naiveRoute() {
  return [...hospitals].sort(
    (first, second) => first.travelMinutes - second.travelMinutes,
  )[0];
}

function getRouteIssues(hospital, patientCase) {
  const issues = [];

  if (hospital.beds <= 0) {
    issues.push("no available beds");
  }

  if (!hospital.services.includes(patientCase.requiredService)) {
    issues.push(`no ${patientCase.requiredService} service`);
  }

  return issues;
}

function capacityPenalty(hospital) {
  return Math.max(0, 5 - hospital.beds) * 3;
}

function recommendRoute(patientCase) {
  const eligibleHospitals = hospitals.filter(
    (hospital) =>
      hospital.beds > 0 &&
      hospital.services.includes(patientCase.requiredService),
  );

  if (eligibleHospitals.length === 0) {
    return null;
  }

  return eligibleHospitals
    .map((hospital) => ({
      hospital,
      penalty: capacityPenalty(hospital),
      score: hospital.travelMinutes + capacityPenalty(hospital),
    }))
    .sort((first, second) => first.score - second.score)[0];
}

function renderCaseList() {
  caseList.innerHTML = cases
    .map(
      (patientCase) => `
        <button
          class="case-button ${patientCase.id === selectedCaseId ? "active" : ""}"
          data-case-id="${patientCase.id}"
          type="button"
        >
          <span class="case-topline">
            <strong>${patientCase.id}</strong>
            <span class="triage ${patientCase.triage.toLowerCase()}">${patientCase.triage}</span>
          </span>
          <span class="case-meta">${patientCase.requiredService} | ${patientCase.status}</span>
        </button>
      `,
    )
    .join("");

  document.querySelectorAll("[data-case-id]").forEach((button) => {
    button.addEventListener("click", () => {
      selectedCaseId = button.dataset.caseId;
      renderAll();
    });
  });
}

function renderSelectedCase() {
  const patientCase = getSelectedCase();
  const nearest = naiveRoute();
  const nearestIssues = getRouteIssues(nearest, patientCase);
  const recommendation = recommendRoute(patientCase);
  const isPending = patientCase.status === "Pending";

  selectedCasePanel.innerHTML = `
    <div class="selected-topline">
      <div>
        <strong>${patientCase.id}</strong>
        <p class="muted">${patientCase.ageGroup} | ${patientCase.requiredService}</p>
      </div>
      <span class="triage ${patientCase.triage.toLowerCase()}">${patientCase.triage}</span>
    </div>
    <p>${patientCase.summary}</p>
    <p class="muted">${patientCase.aiReason}</p>
    <p><strong>Status:</strong> ${patientCase.status}</p>
  `;

  naiveRoutePanel.innerHTML = `
    <p><strong>${nearest.name}</strong> (${nearest.travelMinutes} min)</p>
    <p class="${nearestIssues.length > 0 ? "warning" : "success"}">
      ${
        nearestIssues.length > 0
          ? `Fails because: ${nearestIssues.join(" and ")}.`
          : "Works by chance, but ignores network constraints."
      }
    </p>
  `;

  if (recommendation) {
    smartRoutePanel.innerHTML = `
      <p><strong>${recommendation.hospital.name}</strong></p>
      <p>${recommendation.hospital.travelMinutes} travel minutes + ${recommendation.penalty} capacity penalty = <strong>${recommendation.score}</strong></p>
      <p class="success">Eligible: available bed and matching ${patientCase.requiredService} service.</p>
      <p class="muted">Lower simulation score ranks first. This is not a clinical rule.</p>
    `;
  } else {
    smartRoutePanel.innerHTML = `
      <p class="warning">No eligible destination.</p>
      <p class="muted">No hospital has both an available bed and the required ${patientCase.requiredService} service. Escalation is required in this simulation.</p>
    `;
  }

  const hospitalsWithCapacity = hospitals.filter((hospital) => hospital.beds > 0);

  overrideHospitalSelect.innerHTML = hospitalsWithCapacity
    .map(
      (hospital) => `
        <option
          value="${hospital.id}"
          ${recommendation?.hospital.id === hospital.id ? "selected" : ""}
        >
          ${hospital.name} | ${hospital.services.join("/")}
        </option>
      `,
    )
    .join("");

  approveRouteButton.disabled = !recommendation || !isPending;
  overrideHospitalSelect.disabled = hospitalsWithCapacity.length === 0 || !isPending;
  overrideRouteButton.disabled = hospitalsWithCapacity.length === 0 || !isPending;
}

function renderHospitals() {
  hospitalBody.innerHTML = hospitals
    .map(
      (hospital) => `
        <tr>
          <td><strong>${hospital.name}</strong></td>
          <td>${hospital.travelMinutes} min</td>
          <td class="${hospital.beds === 0 ? "full" : ""}">${hospital.beds}</td>
          <td>${hospital.services.join(", ")}</td>
        </tr>
      `,
    )
    .join("");
}

function renderDecisionLog() {
  if (decisionLog.length === 0) {
    decisionLogPanel.innerHTML = "<li>No decisions recorded yet.</li>";
    return;
  }

  decisionLogPanel.innerHTML = decisionLog
    .map(
      (entry) => `
        <li>
          <strong>${entry.caseId}</strong> routed to <strong>${entry.hospitalName}</strong>
          by ${entry.decisionType} at ${entry.time}.
        </li>
      `,
    )
    .join("");
}

function commitDecision(decisionType, hospitalId) {
  const patientCase = getSelectedCase();
  const hospital = hospitals.find((item) => item.id === hospitalId);

  if (!hospital || hospital.beds <= 0 || patientCase.status !== "Pending") {
    return;
  }

  hospital.beds -= 1;
  patientCase.status = `Dispatched to ${hospital.name}`;

  decisionLog.unshift({
    caseId: patientCase.id,
    hospitalName: hospital.name,
    decisionType,
    time: new Date().toLocaleTimeString(),
  });

  renderAll();
}

approveRouteButton.addEventListener("click", () => {
  const recommendation = recommendRoute(getSelectedCase());

  if (recommendation) {
    commitDecision("operator approval", recommendation.hospital.id);
  }
});

overrideRouteButton.addEventListener("click", () => {
  commitDecision("operator override", overrideHospitalSelect.value);
});

function renderAll() {
  renderCaseList();
  renderSelectedCase();
  renderHospitals();
  renderDecisionLog();
}

renderAll();

That completes the main application. Your Control Centre now turns an explainable recommendation into a human-reviewed decision with an immediate capacity update plus a visible audit trail.

Secret mission

Run a Capacity-Collapse Drill

Your dashboard can route each case while one suitable bed remains. Run three sequential dispatches to exhaust the fictional hospital network. Then identify why every case requires escalation when capacity reaches zero.

Clean Up Your Resources

Clean Up Your Resources

Your cleanup choices are keeping the dashboard available, pausing it by closing its apps, or deleting its folder. This local project has no ongoing cost.

Resources you used:

  • Local emergency-control-centre folder on your Desktop containing index.html, styles.css, and app.js.

No cloud resources, accounts, or billable services need cleanup.

Keep everything running

Keeping everything is useful while you are demonstrating the routing console or experimenting with its synthetic network.

  • Keep the emergency-control-centre folder on your Desktop.
  • Continue showing the completed dashboard in Microsoft Edge.
  • Continue editing the source files in Visual Studio Code.

Your Control Centre remains ready to demonstrate whenever you need it. Keeping the files costs nothing.

Pause - I'll come back to this later

Pausing closes the open apps while preserving every project file. Your dashboard remains available with no ongoing cost.

  • Close Microsoft Edge.
  • Close Visual Studio Code.
  • Leave the emergency-control-centre folder on your Desktop.

That gives you a clean stopping point. Your Control Centre files remain ready for your next session.

Delete - I don't want to use this again

Deleting your project can feel final. Windows File Explorer removes only the local emergency-control-centre folder from your Desktop.

  • Close Microsoft Edge.
  • Close Visual Studio Code.
  • Press the Windows key to open the search bar.
  • Type File Explorer in the search field.
  • Press Enter to open Windows File Explorer.
  • Select Desktop in the left navigation pane.
  • Select the emergency-control-centre folder.
  • Press the Delete key.

You should no longer see the emergency-control-centre folder on your Desktop. You have cleared the full local project without touching any online resources.

Nice Work!

Nice Work!

Outstanding work. Your local Emergency Control Centre now turns fictional cases into transparent routing recommendations that remain subject to human review.

You've learned how to:

  • Built an operations dashboard that displays three fictional emergency cases. Used synthetic data to visualize changing hospital capacity without personal health information.
  • Exposed the nearest-hospital failure for ER-104. Implemented constraint-aware routing that requires an available bed. Added a matching-service check before ranking eligible hospitals with a transparent simulation score.
  • Added human approval controls for recommendations. Added operator overrides for alternate destinations. Recorded each decision in a timestamped audit log. Refreshed hospital capacity after every dispatch.
  • Secret Mission: Stress-tested the network until every required service lost eligible capacity. Identified the escalation condition that replaces automatic routing when no destination can satisfy a case.

Ready to quiz yourself?