Starting Work on an Admin Dashboard

Posted on April 18, 2025

Setting Up a Remote Dashboard

So this post is a bit more informal - now that the site here is up and running, I figured it'd be a good opportunity to start work on a secure admin dashboard that I can access to monitor server information while away from home.

Sure, there are easier ways to monitor a server (Grafana, Netdata, even simple shell scripts) but I wanted to use this as a learning opportunity to get hands-on with Angular and Node.js, and build something custom to my interests. Progress has been slow, mostly because I’m still getting comfortable with JavaScript. That said, I now have a clean, memory-leak-free polling setup that automatically queries my Prometheus container for live server data. So far, I’m using it to show CPU usage, but this setup can easily scale to RAM, disk, network, or any metric Prometheus collects.


Stack Overview

Here’s what I’m working with:

  • Angular (frontend dashboard)
  • Node.js + Express (backend API layer)
  • Prometheus (running in an LXC container on my server)
  • Eventually: MySQL for logging/storing historical events

Right now, everything is running on my local dev machine. Once finalized, I plan to deploy this as multiple containers that talk to each other.


Backend: Creating the Prometheus Endpoint

To get meaningful server stats, I’m using PromQL (Prometheus Query Language) to calculate CPU usage as a percentage. This query gets the average usage across all CPU cores over the last minute:

avg((sum by (cpu) (rate(node_cpu_seconds_total{mode!="idle"}[1m]))) * 100)

Here’s the corresponding Node route:

const express = require('express');
const router = express.Router(); // Creates a router object to handle requests

const { queryPrometheus } = require('../services/prometheus'); // Load the queryPrometheus function from the Prometheus service

// Define a route: GET /api/metrics/cpu
router.get('/cpu-average', async (req, res) => {
  try {
    const raw = await queryPrometheus('avg((sum by (cpu) (rate(node_cpu_seconds_total{mode!="idle"}[1m]))) * 100)'); // Query Prometheus for CPU Usage
    // The query calculates the average CPU usage over the last minute, excluding idle time
    // The result is grouped by CPU and multiplied by 100 to convert it to a percentage
    const [timestamp, value] = raw.data.result[0].value; // Destructure the timestamp and value from the result

    res.json({ // Send the response back to the client
      value: parseFloat(value),
      time: new Date(timestamp * 1000)
    });

  } catch (err) {
    console.error('Error fetching CPU metrics:', err.message);
    res.status(500).json({ error: 'Failed to fetch CPU metrics' });
  }
});

Essentially a straightforward way to send a GET request that hits Prometheus and returns a clean percentage to the frontend.


Polling the Data in Angular

I wanted the frontend to request CPU usage on load and then keep polling for fresh data every 5 seconds. I also wanted to make sure polling stops when the user leaves the page - no memory leaks.

I created a generic PollingService that accepts:

  • A function to fetch data (like an HTTP call)
  • A polling interval in milliseconds
  • A "stop" signal so polling can end cleanly
import { Injectable } from '@angular/core';
import { Observable, interval, merge, of, Subject } from 'rxjs';
import { switchMap, takeUntil } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class PollingService {
  /**
   * Calls fetchFunction every `delay` ms, starting immediately.
   * Stops when stopSignal emits.
   */
  poll<T>(
    fetchFunction: () => Observable<T>,
    delay: number,
    stopSignal: Subject<void>
  ): Observable<T> {
    return merge(of(0), interval(delay)).pipe(
      takeUntil(stopSignal),
      switchMap(fetchFunction)
    );
  }
}

Inside my dashboard component:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { MetricsService } from '../../services/metrics.service';
import { PollingService } from '../../services/polling.service';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';

@Component({
  selector: 'app-dashboard',
  imports: [CommonModule], // CommonModule is imported to use Angular's common directives like ngIf, ngFor
  standalone: true, // This component is standalone, meaning it can be used without being declared in a module
  templateUrl: './dashboard.component.html',
  styleUrl: './dashboard.component.scss'
})
export class DashboardComponent implements OnInit, OnDestroy {
  cpuUsage: number | null = null; // Property to hold CPU usage data, initialized to null
  private stopPolling$ = new Subject<void>(); // Subject to signal when to stop polling

  constructor(
    private metricsService: MetricsService,
    private pollingService: PollingService
  ) {} // Constructor to inject the MetricsService and PollingService

  ngOnInit(): void {
    this.pollingService
      .poll(() => this.metricsService.getCpuAverage(), 5000, this.stopPolling$)
      .subscribe({
        next: (res) => {
          this.cpuUsage = res.value;
        },
        error: (err) => {
          console.error('Polling error:', err);
        }
      });
  }

  ngOnDestroy(): void {
    this.stopPolling$.next();
    this.stopPolling$.complete();
  }
}

The key here is that we use RxJS to merge an immediate value (of(0)) and a recurring timer (interval(...)) to call our function regularly. switchMap() ensures we cancel the last request if a new one starts, and takeUntil() stops it all cleanly.


Showing the Result

The HTML is minimal for now, but it works:

<h2>Server Metrics</h2>
<div> <!-- List of Server Metrics -->
    <p>CPU Usage: {{ cpuUsage !== null ? cpuUsage.toFixed(2) + '%' : 'Loading...' }}</p>

Angular updates this every time cpuUsage is refreshed.


What’s Next?

Now that this core is working:

  • I’ll add memory and disk stats using the same pattern
  • Wrap each stat in a reusable UI card
  • Maybe display usage graphs over time using Chart.js or ngx-charts
  • Eventually build a proper auth/login system so I can access this dashboard securely from outside the network