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.
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:
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.
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);
});
}
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.
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:
canvas.width to match its CSS width so pixels are not stretched// 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).
moveTo, lineTo, arc, fillText — demystifies what those libraries do internally. The entire chart function in dashboard.js is about 30 lines.
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:
/products/widget)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.
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.
#/overview?start=2026-01-01&end=2026-01-31). This would let users bookmark or share a link to a specific date range view.
The full initialization flow runs every time the page loads or the hash changes:
Key points about this flow:
checkAuth() makes a request to /api/dashboard. If it returns 401, the user is redirected to the login page. This prevents unauthenticated users from seeing a broken dashboard.renderOverview() uses Promise.all to fetch dashboard summary data and pageview data simultaneously. This halves the perceived load time compared to sequential requests.route() function reads window.location.hash and calls the appropriate render function. Currently only #/overview is implemented; #/performance, #/errors, and #/admin will be added in later modules.route() again, which re-fetches all data with the new date range and re-renders the entire view.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.