CAPSTONE 2 OF 3

Real-Time Data Monitor

A live server-metrics dashboard — Deferred polling engine, dojox/charting live line chart, pub/sub alert system, start/stop/reset controls, threshold-based color coding

Deferred Polling dojox/charting Live Updates Alert System Pub/Sub
42%
CPU Usage
78%
Memory
94%
Disk I/O

C2.1 — Architecture Overview

PollingEngine ──poll every N sec──▶ /api/metrics │ on data metrics/update ──────────────────▶ MetricCards (update numbers) ──────────────────▶ LiveChart (add data point) metrics/alert ──────────────────▶ AlertPanel (show/clear warnings) │ threshold breach ThresholdChecker ── publishes ──────▶ metrics/alert

Key architectural choice: PollingEngine knows nothing about the chart or the metric cards. It fetches data and publishes to metrics/update. Each display module subscribes independently — swap or add displays without touching PollingEngine.

C2.2 — Deferred Polling Engine Phase 5 – Deferred

// monitor/PollingEngine.js
define([
  "dojo/_base/declare",
  "dojo/_base/lang",
  "dojo/Deferred",
  "dojo/request/xhr",
  "dojo/topic",
  "dojo/Stateful",
  "monitor/thresholds"
], function(declare, lang, Deferred, xhr, topic, Stateful, THRESHOLDS) {

  return declare([Stateful], {

    // Observable state — widgets watch these
    running:     false,
    intervalSec: 3,       // poll every 3 seconds
    pollCount:   0,
    lastError:   null,

    _timer:      null,
    _history:    null,    // ring buffer of last 60 data points
    HISTORY_MAX: 60,

    constructor: function() {
      this._history = [];
    },

    // ── Start polling ─────────────────────────────────────────────
    start: function() {
      if (this.get("running")) return;
      this.set("running", true);
      this.set("lastError", null);
      this._poll();
    },

    // ── Stop polling ──────────────────────────────────────────────
    stop: function() {
      this.set("running", false);
      if (this._timer) { clearTimeout(this._timer); this._timer = null; }
    },

    // ── Reset ─────────────────────────────────────────────────────
    reset: function() {
      this.stop();
      this._history = [];
      this.set("pollCount", 0);
      this.set("lastError", null);
      topic.publish("metrics/reset");
    },

    // ── Core poll function — uses Deferred chaining ───────────────
    _poll: function() {
      if (!this.get("running")) return;

      var self = this;
      var count = this.get("pollCount") + 1;
      this.set("pollCount", count);

      this._fetch()
        .then(function(metrics) {
          // Add to history ring buffer
          self._history.push(metrics);
          if (self._history.length > self.HISTORY_MAX) {
            self._history.shift();
          }

          // Publish live data to all subscribers
          topic.publish("metrics/update", {
            metrics:   metrics,
            history:   self._history.slice(),
            pollCount: count,
            timestamp: Date.now()
          });

          // Check thresholds — publish alerts if breached
          self._checkThresholds(metrics);

          // Schedule next poll
          if (self.get("running")) {
            self._timer = setTimeout(function() { self._poll(); },
                                     self.intervalSec * 1000);
          }
        })
        .otherwise(function(err) {
          self.set("lastError", err.message);
          topic.publish("metrics/error", { error: err, count: count });

          // Retry with backoff — 3× normal interval on error
          if (self.get("running")) {
            self._timer = setTimeout(function() { self._poll(); },
                                     self.intervalSec * 3000);
          }
        });
    },

    // ── Fetch metrics from server ─────────────────────────────────
    // In production: real XHR call
    // In this demo: simulated data with realistic variation
    _fetch: function() {
      var deferred = new Deferred();

      // Simulate async server call
      setTimeout(function() {
        // Realistic simulation: random walk from previous values
        var prev = this._history.length
          ? this._history[this._history.length - 1]
          : { cpu: 35, memory: 62, disk: 45, network: 28 };

        function walk(v, min, max, step) {
          return Math.max(min, Math.min(max, v + (Math.random() - 0.48) * step));
        }

        deferred.resolve({
          cpu:     Math.round(walk(prev.cpu,     0, 100, 8)),
          memory:  Math.round(walk(prev.memory,  40, 98,  4)),
          disk:    Math.round(walk(prev.disk,    10, 100, 12)),
          network: Math.round(walk(prev.network,  0,  80,  6)),
          timestamp: Date.now()
        });
      }.bind(this), 200 + Math.random() * 300);  // 200-500ms simulated latency

      return deferred.promise;

      // Real XHR version:
      // return xhr.get("/api/metrics", { handleAs: "json" });
    },

    // ── Threshold checking ────────────────────────────────────────
    _checkThresholds: function(metrics) {
      var alerts = [];

      Object.keys(THRESHOLDS).forEach(function(key) {
        var t = THRESHOLDS[key];
        var val = metrics[key];
        if (val === undefined) return;

        var level = null;
        if (val >= t.critical) level = "critical";
        else if (val >= t.warning) level = "warning";

        if (level) {
          alerts.push({ metric: key, value: val, level: level, threshold: t[level] });
        }
      });

      if (alerts.length) {
        topic.publish("metrics/alert", { alerts: alerts, metrics: metrics });
      } else {
        topic.publish("metrics/allClear");
      }
    }
  });
});

// monitor/thresholds.js
define({
  cpu:     { warning: 70, critical: 90 },
  memory:  { warning: 75, critical: 90 },
  disk:    { warning: 80, critical: 95 },
  network: { warning: 60, critical: 80 }
});

C2.3 — dojox/charting Live Chart Phase 6 – Pub/Sub

// monitor/LiveChart.js
define([
  "dojo/_base/declare",
  "dojo/_base/lang",
  "dijit/_Widget",
  "dojox/charting/Chart",
  "dojox/charting/axis2d/Default",
  "dojox/charting/plot2d/Lines",
  "dojox/charting/themes/Julie",
  "dojo/topic",
  "dojo/dom-construct"
], function(declare, lang, _Widget,
            Chart, Default, Lines, theme,
            topic, domConstruct) {

  return declare([_Widget], {

    MAX_POINTS: 60,   // keep 60 data points (= MAX_POINTS × intervalSec of history)

    _chart:    null,
    _series:   null,  // { cpu: [], memory: [], disk: [], network: [] }

    buildRendering: function() {
      this.domNode = domConstruct.create("div", {
        style: "width:100%; height:300px;"
      });
    },

    startup: function() {
      if (this._started) return;
      this.inherited(arguments);

      this._series = { cpu: [], memory: [], disk: [], network: [] };

      // ── Create chart ───────────────────────────────────────────
      this._chart = new Chart(this.domNode);
      this._chart.setTheme(theme);

      this._chart.addAxis("x", {
        title:        "Time (polls)",
        natural:      true,
        majorLabels:  false,
        minorLabels:  false
      });

      this._chart.addAxis("y", {
        title:    "% Utilisation",
        vertical: true,
        min:      0,
        max:      100,
        majorTick: { stroke: "rgba(255,255,255,.08)", length: 5 }
      });

      this._chart.addPlot("default", {
        type:        Lines,
        tension:     "S",       // smooth cubic spline
        markers:     false,
        shadows:     { dx:1, dy:1, dw:2 }
      });

      // Add one series per metric
      var colors = { cpu:"#6366f1", memory:"#22d3ee", disk:"#f472b6", network:"#34d399" };
      Object.keys(this._series).forEach(lang.hitch(this, function(key) {
        this._chart.addSeries(key, [0], {
          stroke: { color: colors[key], width: 2 },
          fill:   "none"
        });
      }));

      this._chart.render();

      // ── Subscribe to live data ──────────────────────────────────
      this.own(
        topic.subscribe("metrics/update", lang.hitch(this, "_onData")),
        topic.subscribe("metrics/reset",  lang.hitch(this, "_onReset"))
      );
    },

    _onData: function(data) {
      var m = data.metrics;

      // Append new values, trim to MAX_POINTS
      Object.keys(this._series).forEach(lang.hitch(this, function(key) {
        this._series[key].push(m[key]);
        if (this._series[key].length > this.MAX_POINTS) {
          this._series[key].shift();
        }
        // Update chart series with new data
        this._chart.updateSeries(key, this._series[key]);
      }));

      // Animate chart update
      this._chart.render();
    },

    _onReset: function() {
      Object.keys(this._series).forEach(lang.hitch(this, function(key) {
        this._series[key] = [];
        this._chart.updateSeries(key, [0]);
      }));
      this._chart.render();
    },

    destroy: function(preserveDom) {
      if (this._chart) { this._chart.destroy(); this._chart = null; }
      this.inherited(arguments);
    }
  });
});

C2.4 — Alert System via Pub/Sub Phase 6 – Pub/Sub

// monitor/AlertPanel.js
define([
  "dojo/_base/declare",
  "dojo/_base/lang",
  "dijit/_Widget",
  "dojo/topic",
  "dojo/dom-construct",
  "dojo/dom-class"
], function(declare, lang, _Widget, topic, domConstruct, domClass) {

  return declare([_Widget], {

    buildRendering: function() {
      this.domNode = domConstruct.create("div", { className: "alert-panel" });
      this._list = domConstruct.create("ul", {}, this.domNode);
    },

    postCreate: function() {
      this.inherited(arguments);
      this.own(
        topic.subscribe("metrics/alert",    lang.hitch(this, "_showAlerts")),
        topic.subscribe("metrics/allClear", lang.hitch(this, "_clearAlerts")),
        topic.subscribe("metrics/reset",    lang.hitch(this, "_clearAlerts"))
      );
    },

    _showAlerts: function(data) {
      domConstruct.empty(this._list);
      domClass.remove(this.domNode, "alert-panel--clear");

      data.alerts.forEach(lang.hitch(this, function(alert) {
        var isCrit = alert.level === "critical";
        var li = domConstruct.create("li", {
          className: "alert-item alert-item--" + alert.level,
          innerHTML:
            "" + (isCrit ? "🔴" : "🟡") + "" +
            "" + alert.metric.toUpperCase() + " at " +
            alert.value + "% " +
            "(threshold: " + alert.threshold + "%)"
        }, this._list);

        // Critical alerts pulse
        if (isCrit) domClass.add(li, "alert-pulse");
      }));
    },

    _clearAlerts: function() {
      domConstruct.empty(this._list);
      domConstruct.create("li", {
        className: "alert-item alert-item--ok",
        innerHTML: "✅ All metrics within normal range"
      }, this._list);
      domClass.add(this.domNode, "alert-panel--clear");
    }
  });
});

C2.5 — Start / Stop / Reset Controls Phase 4 – Dijit Phase 3 – Stateful

// monitor/ControlBar.js
define([
  "dojo/_base/declare",
  "dojo/_base/lang",
  "dijit/_Widget",
  "dijit/form/Button",
  "dijit/form/NumberSpinner",
  "dojo/dom-construct",
  "dojo/dom-class"
], function(declare, lang, _Widget, Button, NumberSpinner, domConstruct, domClass) {

  return declare([_Widget], {

    engine: null,   // PollingEngine instance passed in

    buildRendering: function() {
      this.domNode = domConstruct.create("div", { className:"control-bar" });
    },

    postCreate: function() {
      this.inherited(arguments);
      var self = this;
      var engine = this.engine;

      // ── Start/Stop button (toggle) ────────────────────────────
      this._startStopBtn = new Button({
        label:    "▶ Start",
        "class": "btn-primary",
        onClick:  function() {
          if (engine.get("running")) {
            engine.stop();
          } else {
            engine.start();
          }
        }
      }).placeAt(this.domNode);
      this._startStopBtn.startup();

      // ── Reset button ──────────────────────────────────────────
      new Button({
        label:   "↺ Reset",
        onClick: function() { engine.reset(); }
      }).placeAt(this.domNode).startup();

      // ── Interval selector ─────────────────────────────────────
      domConstruct.create("label", {
        textContent: "Poll every",
        style: "margin-left:16px; margin-right:6px; color:var(--text2)"
      }, this.domNode);

      var intervalSpinner = new NumberSpinner({
        value:       engine.get("intervalSec"),
        constraints: { min:1, max:30, places:0 },
        smallDelta:  1,
        style:       "width:70px",
        onChange: function(v) { engine.set("intervalSec", v); }
      }).placeAt(this.domNode);
      intervalSpinner.startup();

      domConstruct.create("span", {
        textContent: "seconds",
        style: "margin-left:6px; color:var(--muted)"
      }, this.domNode);

      // ── Poll counter display ──────────────────────────────────
      this._counter = domConstruct.create("span", {
        style: "margin-left:20px; color:var(--muted); font-size:.85rem"
      }, this.domNode);

      // ── Watch engine state → update button label ──────────────
      this.own(
        engine.watch("running", lang.hitch(this, function(p, old, running) {
          this._startStopBtn.set("label", running ? "�?� Pause" : "▶ Start");
          domClass.toggle(this._startStopBtn.domNode, "btn-danger", running);
          domClass.toggle(this._startStopBtn.domNode, "btn-primary", !running);
        })),
        engine.watch("pollCount", lang.hitch(this, function(p, o, count) {
          this._counter.textContent = "Polls: " + count;
        })),
        engine.watch("lastError", lang.hitch(this, function(p, o, err) {
          if (err) {
            this._counter.textContent = "Error: " + err;
            this._counter.style.color = "#f87171";
          }
        }))
      );
    }
  });
});

C2.6 — Metric Cards with Stateful Color Coding Phase 3 – Stateful

// monitor/MetricCards.js
define([
  "dojo/_base/declare",
  "dojo/_base/lang",
  "dijit/_Widget",
  "dojo/topic",
  "dojo/dom-construct",
  "dojo/dom-class",
  "monitor/thresholds"
], function(declare, lang, _Widget, topic, domConstruct, domClass, THRESHOLDS) {

  return declare([_Widget], {

    _cards: null,

    buildRendering: function() {
      this.domNode = domConstruct.create("div", { className:"metric-cards-grid" });
      this._cards  = {};
    },

    postCreate: function() {
      this.inherited(arguments);

      // Build a card for each metric
      ["cpu", "memory", "disk", "network"].forEach(lang.hitch(this, function(key) {
        var card = domConstruct.create("div", {
          className: "metric-card",
          innerHTML:
            "
" + "
" + key.toUpperCase() + "
" + "
" }, this.domNode); this._cards[key] = { card: card, value: card.querySelector("#val-" + key), bar: card.querySelector("#bar-" + key) }; })); this.own( topic.subscribe("metrics/update", lang.hitch(this, "_update")), topic.subscribe("metrics/reset", lang.hitch(this, "_reset")) ); }, _update: function(data) { var metrics = data.metrics; Object.keys(this._cards).forEach(lang.hitch(this, function(key) { var val = metrics[key]; var card = this._cards[key]; if (val === undefined) return; // Update display value card.value.textContent = val + "%"; // Update progress bar width card.bar.style.width = val + "%"; // Colour-code based on thresholds var t = THRESHOLDS[key]; domClass.remove(card.card, "metric-ok metric-warn metric-crit"); if (val >= t.critical) domClass.add(card.card, "metric-crit"); else if (val >= t.warning) domClass.add(card.card, "metric-warn"); else domClass.add(card.card, "metric-ok"); })); }, _reset: function() { Object.keys(this._cards).forEach(lang.hitch(this, function(key) { this._cards[key].value.textContent = "—"; this._cards[key].bar.style.width = "0%"; domClass.remove(this._cards[key].card, "metric-ok metric-warn metric-crit"); })); } }); });

C2.7 — Complete Wiring Phase 1 – AMD

// monitor/boot.js
define([
  "dojo/_base/declare",
  "dijit/layout/BorderContainer",
  "dijit/layout/ContentPane",
  "monitor/PollingEngine",
  "monitor/ControlBar",
  "monitor/MetricCards",
  "monitor/LiveChart",
  "monitor/AlertPanel",
  "dojo/domReady!"
], function(
  declare,
  BorderContainer, ContentPane,
  PollingEngine, ControlBar, MetricCards, LiveChart, AlertPanel
) {

  // ── Create the single shared polling engine ────────────────────
  var engine = new PollingEngine({ intervalSec: 3 });

  // ── Full-page layout ───────────────────────────────────────────
  var layout = new BorderContainer({
    design: "headline",
    style:  "width:100%;height:100vh;"
  });

  // Top: control bar (start/stop/reset + interval + poll count)
  var topPane = new ContentPane({
    region:  "top",
    style:   "height:56px; padding:0 16px;",
    content: new ControlBar({ engine: engine })
  });

  // Left: metric cards + alert panel
  var leftPane = new ContentPane({ region:"left", style:"width:280px;", splitter:true });
  new MetricCards().placeAt(leftPane.containerNode);
  new AlertPanel().placeAt(leftPane.containerNode);

  // Center: live chart
  var centerPane = new ContentPane({
    region:  "center",
    content: new LiveChart()
  });

  layout.addChild(topPane);
  layout.addChild(leftPane);
  layout.addChild(centerPane);

  layout.placeAt(document.body);
  layout.startup();

  // Auto-start polling on load
  engine.start();
});

Capstone 2 — Key Patterns Demonstrated

PatternWhere Used
Deferred for async polling loopPollingEngine._poll() — chained fetch + schedule next
Deferred for simulated latencyPollingEngine._fetch() — setTimeout inside Deferred
Stateful for observable engine staterunning, pollCount watched by ControlBar
Pub/sub one-to-many fanoutmetrics/update → 3 subscribers (cards, chart, threshold checker)
Deferred error recovery with backoff.otherwise() in poll loop — retries at 3× interval
dojox/charting live data pushchart.updateSeries() then chart.render() on each data point
domClass for threshold colour codingMetricCards toggles ok/warn/crit CSS classes per metric

Capstone 3 — Multi-Step Form Wizard

Focuses on StackContainer navigation, per-step validation, draft persistence, and a custom progress indicator widget.

Continue to Capstone 3 →
Capstone Apps
Sections
CAPSTONE 2 STATS

Modules: 7
Topics: 4
Key phases: 3, 5, 6
Chart: dojox/charting