Dashboard

The dashboard is where analytics data becomes visual and actionable. It is a single-page application that consumes the reporting API and renders charts, tables, and summary cards. Users log in, select date ranges, and explore their site's performance, errors, and traffic.

1. What the Dashboard Shows

The dashboard provides four main views, each focused on a different aspect of your analytics data. Every view fetches its data from the corresponding reporting API endpoint and renders it client-side.

Overview

The landing page after login. It presents a high-level snapshot of site activity: summary cards showing total page views, unique sessions, average load time, and error count for the selected date range. Below the cards, a line chart plots page views over time (one data point per day). At the bottom, a top pages table lists the most-visited URLs ranked by view count.

API endpoint: GET /api/overview?start=...&end=...

Performance Report

Focuses on Core Web Vitals and load times. Displays median and p75 values for LCP, CLS, INP, FCP, TTFB, and full load time. A bar chart compares these metrics against Google's "good," "needs improvement," and "poor" thresholds. A per-page breakdown table shows which pages are fastest and which are dragging down the averages.

API endpoint: GET /api/performance?start=...&end=...

Error Report

Shows JavaScript error frequency and trends. A line chart plots error count per day. A ranked table groups errors by message, showing occurrence count, most-affected page, and most-affected browser. Clicking an error message reveals the full stack trace and a timeline of when the error first and last appeared.

API endpoint: GET /api/errors?start=...&end=...

Admin

User management panel, restricted to owner and admin roles. Lists all dashboard accounts with their email, display name, role, and last login date. Allows creating new users, changing roles, and deleting accounts. The owner account cannot be deleted or demoted.

API endpoint: GET /api/users, POST /api/users, PUT /api/users/:id, DELETE /api/users/:id

2. SPA Architecture

The dashboard is a single-page application (SPA). One HTML file is served to the browser. JavaScript handles all routing, data fetching, and rendering. The page never fully reloads after the initial load.

How It Works

The browser loads dashboard.html once. A JavaScript router listens for hashchange events on window.location.hash. When the hash changes (e.g., from #/overview to #/errors), the router calls the corresponding view function. That function uses fetch() to request data from the reporting API, then renders HTML into the main content area using DOM manipulation.

dashboard.html | v ┌──────────────────────────────────────────────────────┐ │ Router (hashchange listener) │ │ │ │ #/overview --> overviewView() │ │ #/performance --> performanceView() │ │ #/errors --> errorsView() │ │ #/admin --> adminView() │ │ #/login --> loginView() │ └──────────────────────────────────────────────────────┘ | | | v v v ┌─────────┐ ┌─────────────┐ ┌───────────┐ │ fetch() │ │ fetch() │ │ fetch() │ │ /api/ │ │ /api/ │ │ /api/ │ │ overview│ │ performance │ │ errors │ └────┬────┘ └──────┬──────┘ └─────┬─────┘ | | | v v v ┌──────────────────────────────────────────────────────┐ │ Render: update DOM with cards, charts, tables │ └──────────────────────────────────────────────────────┘

No server-side rendering is involved. The server's only job is to serve the static HTML/JS/CSS files and respond to API requests. This separation makes the dashboard a pure API consumer — it could be hosted on any static file server.

Why Hash-Based Routing?

Hash fragments (#/overview) do not trigger a server request. The browser handles them entirely on the client side. This means no server configuration is needed to handle client-side routes — no rewrite rules, no catch-all routes. The server sees every request as a request for dashboard.html, regardless of the hash.

3. Screen Inventory

Each screen in the dashboard maps to a hash route, an API endpoint, and a set of UI components. The following table is the complete inventory:

Screen API Endpoint Key Components User Roles
Login POST /api/login Email input, password input, submit button, error message area All (unauthenticated)
Overview GET /api/overview 4 summary cards, line chart (pageviews/day), top pages table owner, admin, viewer
Performance Report GET /api/performance Web Vitals gauges, bar chart (metric distribution), per-page table owner, admin, viewer
Error Report GET /api/errors Error trend line chart, grouped error table, stack trace detail panel owner, admin, viewer
Admin Panel GET/POST/PUT/DELETE /api/users User list table, create user form, role dropdown, delete button owner, admin
Role-based visibility: The router checks the logged-in user's role before rendering the Admin screen. If a viewer navigates to #/admin, they are redirected to #/overview. The API also enforces this server-side — client-side checks are for UX, not security.

4. Chart Strategy

The dashboard needs two chart types: line charts for trends over time (page views per day, error frequency) and bar charts for comparisons (Web Vitals distribution, top pages). That is it. No pie charts, no scatter plots, no complex visualizations.

Canvas API Directly

The HTML <canvas> element provides a 2D drawing context. You can draw lines, rectangles, text, and arcs with simple JavaScript calls. For a line chart, the steps are:

  1. Map data points to canvas pixel coordinates (scale x by date range, y by max value)
  2. Draw axes with ctx.moveTo() and ctx.lineTo()
  3. Draw axis labels with ctx.fillText()
  4. Plot data points by iterating over the array and calling ctx.lineTo() for each point
  5. Stroke the path with ctx.stroke()

For a bar chart, replace the line path with ctx.fillRect() calls for each bar.

Library Alternatives

Option Size Tradeoff
Vanilla Canvas 0 KB (built-in) Full control, educational value, more code to write
Chart.js ~200 KB Feature-rich, well-documented, responsive out of the box
uPlot ~35 KB Extremely fast rendering, time-series focused, minimal API
For this project, we use vanilla Canvas. The educational value of understanding how charts are drawn — coordinate mapping, axis scaling, label positioning — outweighs the convenience of a library. Once you understand the fundamentals, adopting Chart.js or uPlot later takes minutes.

5. Layout & Navigation

The dashboard uses a classic sidebar + header + content layout built with CSS Grid. The sidebar provides navigation between views. The header displays the logo, a date range picker, and user account controls. The main content area renders whichever view is currently active.

┌─────────────────────────────────────────────────┐ │ Header: Logo Date Range Picker User ▼ Logout │ ├────────┬────────────────────────────────────────┤ │ Nav │ Content Area │ │ │ │ │ Overview│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ Speed │ │Views│ │Sess.│ │Load │ │Errors│ │ │ Errors │ └─────┘ └─────┘ └─────┘ └─────┘ │ │ Admin │ │ │ │ ┌─────────────────────────────────┐ │ │ │ │ Line Chart: Pageviews Over Time │ │ │ │ └─────────────────────────────────┘ │ │ │ │ │ │ ┌─────────────────────────────────┐ │ │ │ │ Table: Top Pages │ │ │ │ └─────────────────────────────────┘ │ ├────────┴────────────────────────────────────────┤ │ Footer │ └─────────────────────────────────────────────────┘

CSS Grid Structure

The layout is defined with a two-column grid. The sidebar occupies a fixed-width column, the content area takes the remaining space, and the header and footer span both columns:

.dashboard-shell {
    display: grid;
    grid-template-columns: 200px 1fr;
    grid-template-rows: auto 1fr auto;
    grid-template-areas:
        "header  header"
        "sidebar content"
        "footer  footer";
    min-height: 100vh;
}

.dashboard-header  { grid-area: header; }
.dashboard-sidebar { grid-area: sidebar; }
.dashboard-content { grid-area: content; }
.dashboard-footer  { grid-area: footer; }

Sidebar Navigation

The sidebar contains anchor links that set the URL hash: <a href="#/overview">Overview</a>, <a href="#/performance">Speed</a>, etc. The router highlights the active link by comparing the current hash to each link's href. No page reload occurs — only the content area updates.

Header Components

6. Responsive Design

The dashboard must work on tablets and phones, not just desktop monitors. The layout adapts at a 768px breakpoint.

Desktop (above 768px)

Mobile (768px and below)

/* Desktop: sidebar + content side by side */
.dashboard-shell {
    grid-template-columns: 200px 1fr;
}

/* Mobile: single column, sidebar hidden */
@media (max-width: 768px) {
    .dashboard-shell {
        grid-template-columns: 1fr;
        grid-template-areas:
            "header"
            "content"
            "footer";
    }

    .dashboard-sidebar {
        display: none;  /* toggled via JS hamburger button */
    }

    .dashboard-sidebar.open {
        display: block;
        position: fixed;
        top: 0; left: 0;
        width: 240px; height: 100vh;
        z-index: 200;
        background: #2c3e50;
    }

    .summary-cards {
        grid-template-columns: 1fr;  /* stack vertically */
    }

    .table-wrapper {
        overflow-x: auto;  /* horizontal scroll for wide tables */
    }
}

7. Date Range Filtering

Every data view in the dashboard is filtered by a date range. The date range picker sits in the header and applies globally to whichever view is currently active.

Default Range

When the dashboard loads, the default range is the last 30 days: start date is today minus 30 days, end date is today. The picker displays these defaults and the initial data fetch uses them.

On Change Behavior

When the user changes either date input, the dashboard:

  1. Validates that start ≤ end (show an inline error if not)
  2. Updates the URL hash to include the date range: #/overview?start=2024-01-01&end=2024-01-31
  3. Re-fetches all data for the current view with the new date parameters
  4. Re-renders the charts and tables with the fresh data

Bookmarkability

Storing the date range in the URL hash means users can bookmark a specific view with a specific date range and return to it later. Sharing the URL with a teammate opens the same view with the same dates. The router parses the hash on page load to restore the state:

// Parse hash: #/overview?start=2024-01-01&end=2024-01-31
function parseHash(hash) {
    const [path, query] = hash.replace('#', '').split('?');
    const params = new URLSearchParams(query || '');
    return {
        route: path || '/overview',
        start: params.get('start') || defaultStart(),
        end:   params.get('end')   || defaultEnd()
    };
}

function defaultStart() {
    const d = new Date();
    d.setDate(d.getDate() - 30);
    return d.toISOString().slice(0, 10);
}

function defaultEnd() {
    return new Date().toISOString().slice(0, 10);
}

8. Loading States & Error Handling

Every fetch() call to the reporting API is asynchronous. The user needs visual feedback while data is loading, and clear messaging when something goes wrong.

Loading Indicators

While a fetch is in progress, display a skeleton screen or spinner in the content area. Skeleton screens are preferred because they hint at the structure of the incoming content (gray rectangles where cards will appear, a placeholder box where the chart will render). This reduces perceived wait time.

function showLoading(container) {
    container.innerHTML = `
        <div class="skeleton-cards">
            <div class="skeleton-card"></div>
            <div class="skeleton-card"></div>
            <div class="skeleton-card"></div>
            <div class="skeleton-card"></div>
        </div>
        <div class="skeleton-chart"></div>
    `;
}

async function loadOverview(start, end) {
    const content = document.getElementById('content');
    showLoading(content);

    try {
        const res = await fetch(`/api/overview?start=${start}&end=${end}`);
        if (!res.ok) throw new Error(`API returned ${res.status}`);
        const data = await res.json();
        renderOverview(content, data);
    } catch (err) {
        showError(content, err.message);
    }
}

Error States

When the API returns an error, display a clear message in the content area with the HTTP status code and a retry button. Do not silently fail. Do not show a blank screen.

401 Handling

If any API response returns 401 Unauthorized, the auth token has expired or is invalid. The dashboard should immediately clear the stored token and redirect to the login screen (#/login). Do not show the data view with stale or missing data.

async function apiFetch(url) {
    const token = localStorage.getItem('token');
    const res = await fetch(url, {
        headers: { 'Authorization': `Bearer ${token}` }
    });

    if (res.status === 401) {
        localStorage.removeItem('token');
        window.location.hash = '#/login';
        return null;
    }

    if (!res.ok) {
        throw new Error(`API error: ${res.status} ${res.statusText}`);
    }

    return res.json();
}

9. XSS in Dashboards

An analytics dashboard renders data that originated from untrusted sources: visitor URLs, user-agent strings, error messages, referrer headers, and page titles. Even though this data was sanitized during ingestion at the collector and server-processing layers, defense in depth demands that the dashboard also protect itself.

The Core Rule

Use textContent, not innerHTML, when rendering any value that originated from analytics data. textContent treats the value as plain text and will not execute HTML or JavaScript embedded in it. innerHTML parses the string as HTML, which means a malicious URL like <img src=x onerror=alert(1)> would execute if rendered with innerHTML.

// SAFE: textContent escapes HTML entities automatically
const cell = document.createElement('td');
cell.textContent = row.url;  // "<script>alert(1)</script>" renders as text

// DANGEROUS: innerHTML parses and executes HTML
cell.innerHTML = row.url;  // <script>alert(1)</script> EXECUTES

Additional Rules

Defense in depth: The collector validates and trims input. The server-processing pipeline sanitizes and rejects malformed data. The database stores clean values. The reporting API returns JSON (not HTML). The dashboard uses textContent for all user-sourced fields. Every layer assumes the previous layer might have missed something. That is how you prevent XSS.

Safe DOM Building Pattern

Build table rows and card content using DOM methods instead of HTML string concatenation:

function buildPageRow(page) {
    const tr = document.createElement('tr');

    const urlCell = document.createElement('td');
    urlCell.textContent = page.url;     // safe: never innerHTML
    tr.appendChild(urlCell);

    const viewsCell = document.createElement('td');
    viewsCell.textContent = page.views; // safe: numeric, but still use textContent
    tr.appendChild(viewsCell);

    return tr;
}

// Build the table body safely:
const tbody = document.getElementById('top-pages-body');
tbody.innerHTML = '';  // clear previous rows (safe: no user data)
data.topPages.forEach(page => {
    tbody.appendChild(buildPageRow(page));
});

Tutorial Modules

01. Login & Auth

Build the login screen, authenticate against the reporting API, store the JWT token, and protect dashboard routes from unauthenticated access.

Start Module →

02. Overview Page

Render summary cards, draw a line chart of daily page views on canvas, and build the top pages table with data from the overview API endpoint.

Start Module →

03. Speed & Error Reports

Build the performance report with Web Vitals bar charts and the error report with trend lines, grouped error tables, and stack trace detail panels.

Start Module →

04. Admin Panel

Create the user management interface: list accounts, add new users, change roles, and delete accounts. Enforce role-based access on both client and API.

Start Module →