Module 02: Dashboard Overview Page

The overview page is the first thing users see after login. It displays summary metric cards (total pageviews, sessions, avg load time, errors), a line chart of pageviews over time, and a top pages table. All data comes from the reporting API.

Demo Files

1. Dashboard Shell

The dashboard.html file provides the structural skeleton for the entire dashboard application. It is a single-page application (SPA) shell — one HTML file that never reloads. All content changes happen inside a <div id="content"> container via JavaScript.

The layout uses CSS Grid with three regions:

┌──────────────────────────────────────────────┐ │ Header: title, date pickers, user, logout │ ├──────────┬─────────────────────────────────────┤ │ Sidebar │ │ │ │ Main Content (#content) │ │ Overview │ │ │ Perf │ Cards, Charts, Tables rendered │ │ Errors │ here by JavaScript │ │ Admin │ │ └──────────┴─────────────────────────────────────┘

The sidebar contains navigation links that use hash-based routing:

<nav class="sidebar">
  <a href="#/overview" class="active">Overview</a>
  <a href="#/performance">Performance</a>
  <a href="#/errors">Errors</a>
  <a href="#/admin">Admin</a>
</nav>

Hash-based routing means the browser never makes a new page request when the user clicks a nav link. Instead, JavaScript listens for the hashchange event and swaps out the content inside #content. The routes #/overview, #/performance, #/errors, and #/admin each map to a different rendering function.

The header spans the full width and contains the application title, two date inputs for filtering, the logged-in user's display name, and a logout button.

2. Metric Cards

At the top of the overview page, four summary cards give the user an at-a-glance picture of their site's health:

Card API Field What It Measures
Total Pageviews total_pageviews Number of page loads recorded in the date range
Total Sessions total_sessions Distinct browsing sessions (grouped by session ID)
Avg Load Time avg_load_time_ms Mean page load time in milliseconds
Total Errors total_errors JavaScript errors captured in the date range

The data comes from GET /api/dashboard. Here is how the cards are rendered:

function renderCards(data) {
  const container = document.getElementById('cards');
  container.innerHTML = '';

  const metrics = [
    { label: 'Total Pageviews', value: (data.total_pageviews || 0).toLocaleString() },
    { label: 'Total Sessions',  value: (data.total_sessions || 0).toLocaleString() },
    { label: 'Avg Load Time',   value: (data.avg_load_time_ms || 0) + ' ms' },
    { label: 'Total Errors',    value: (data.total_errors || 0).toLocaleString() },
  ];

  metrics.forEach(m => {
    const card = document.createElement('div');
    card.className = 'metric-card';

    const label = document.createElement('div');
    label.className = 'metric-label';
    label.textContent = m.label;       // textContent, not innerHTML

    const value = document.createElement('div');
    value.className = 'metric-value';
    value.textContent = m.value;       // textContent, not innerHTML

    card.appendChild(label);
    card.appendChild(value);
    container.appendChild(card);
  });
}
XSS Safety: Every value is set via textContent, never innerHTML. If the API ever returned a string containing <script> tags or HTML, textContent would render it as harmless text. This is a fundamental defense against cross-site scripting (XSS) attacks in any dashboard that displays user-generated or external data.

3. Line Chart with Canvas

The overview includes a line chart that plots pageviews by day. Rather than importing a charting library, we draw directly on a <canvas> element. This keeps the dashboard dependency-free and teaches how chart rendering actually works.

The drawing process has five steps:

  1. Size the canvas — Set canvas.width to match its CSS width so pixels are not stretched
  2. Compute scale — Find the maximum data value and map all values to pixel coordinates within the plot area
  3. Draw axes — Two lines: vertical (Y) and horizontal (X), offset by padding
  4. Plot points and connect lines — Loop through data, compute (x, y) for each point, draw a path
  5. Label axes — Show the first, middle, and last dates on X; max value and zero on Y
// Scale a data value to a Y pixel coordinate
const plotH = canvasHeight - padding.top - padding.bottom;
const maxVal = Math.max(...values, 1);
const y = canvasHeight - padding.bottom - (value / maxVal) * plotH;

// Scale a data index to an X pixel coordinate
const plotW = canvasWidth - padding.left - padding.right;
const x = padding.left + (index / (dataPoints.length - 1)) * plotW;

The X-axis represents dates and the Y-axis represents pageview counts. Each data point comes from the /api/pageviews endpoint's byDay array, where each entry has a day (date string) and views (integer count).

Why Canvas Instead of a Library? For a production dashboard, you would likely use a library like Chart.js or D3. But understanding the underlying canvas drawing calls — moveTo, lineTo, arc, fillText — demystifies what those libraries do internally. The entire chart function in dashboard.js is about 30 lines.

4. Top Pages Table

Below the chart, a table lists the most-viewed pages in the selected date range. The data comes from the /api/pageviews endpoint's topPages array.

Each row has two columns:

function renderTable(container, pages) {
  container.innerHTML = '';
  const table = document.createElement('table');
  const thead = document.createElement('thead');
  thead.innerHTML = '<tr><th>URL</th><th>Views</th></tr>';
  table.appendChild(thead);

  const tbody = document.createElement('tbody');
  (pages || []).forEach(p => {
    const tr = document.createElement('tr');
    const tdUrl = document.createElement('td');
    tdUrl.textContent = p.url;                    // XSS safe
    const tdViews = document.createElement('td');
    tdViews.textContent = Number(p.views).toLocaleString();
    tr.appendChild(tdUrl);
    tr.appendChild(tdViews);
    tbody.appendChild(tr);
  });
  table.appendChild(tbody);
  container.appendChild(table);
}

Again, textContent is used for the URL column. URLs are user-controlled data — an attacker could submit a pageview with a URL containing javascript: or <img onerror=...>. By using textContent, the value is always rendered as plain text.

5. Date Range Picker

The header contains two <input type="date"> elements that control the date range for all data on the page:

<input type="date" id="date-start">
<input type="date" id="date-end">

When either input changes, the dashboard re-fetches all data from the API with the new date parameters:

document.addEventListener('change', e => {
  if (e.target.id === 'date-start' || e.target.id === 'date-end') {
    route();  // re-render current view with new dates
  }
});

function getDateRange() {
  const end = document.getElementById('date-end')?.value
    || new Date().toISOString().slice(0, 10);
  const start = document.getElementById('date-start')?.value
    || new Date(Date.now() - 30 * 86400000).toISOString().slice(0, 10);
  return { start, end };
}

The default range is the last 30 days. Dates are appended to every API call as query parameters: /api/dashboard?start=2026-01-01&end=2026-01-31.

Bookmarkability: Because the dates are read from form inputs on every render, you could extend this to store them in the URL hash (e.g., #/overview?start=2026-01-01&end=2026-01-31). This would let users bookmark or share a link to a specific date range view.

6. Putting It Together

The full initialization flow runs every time the page loads or the hash changes:

init() │ ├── checkAuth() │ └── GET /api/dashboard │ ├── 401 → redirect to login.html │ └── 200 → continue │ ├── listen for hashchange → route() │ └── route() └── renderOverview() ├── fetch /api/dashboard ─┐ ├── fetch /api/pageviews ─┤ (parallel) │ │ ├── renderCards(dashboard) ├── renderLineChart(pageviews.byDay) └── renderTable(pageviews.topPages)

Key points about this flow:

async function init() {
  if (await checkAuth()) {
    window.addEventListener('hashchange', route);
    route();
  }
}

The entire dashboard JavaScript is roughly 180 lines, with no external dependencies. It demonstrates the core patterns of a data-driven SPA: fetch data from an API, build DOM elements programmatically, and re-render in response to user actions.