Build a Real-Time Operations Dashboard

Add SignalR live metrics, audit logging, and a unified iBPMS platform shell.

Introduction

30 Second Summary

Think about air traffic control. Controllers need a live view of every plane in the sky, not a report from an hour ago. Operations managers running business workflows have the same need. They need to see what is happening right now across every running process.

In this project, you will build a real-time operations dashboard that monitors your BPM engine, records every workflow event in an audit log, and unifies all three parts of your platform into one runnable application.

What You'll Build

A real-time operations dashboard that mirrors Genpact Cora SeQuence's HotOperations portal, showing live workflow metrics via SignalR WebSockets and recording every action in an audit log persisted to SQL Server.

By the end of this project, you'll have:

  • A SignalR hub that broadcasts workflow lifecycle events to connected dashboard clients in real time.
  • An audit log system that records every workflow action with timestamps, stored in a dedicated database table.
  • A unified iBPMS platform with navigation connecting your Visual Designer, Task Portal, and Operations Dashboard into one application.
  • Secret Mission: Real-time task reallocation from the dashboard with live broadcast to all connected users.

Are there any prerequisites?

Yes. This is Part 3 of the iBPMS Platform series. You need a working project from Part 2 (BPM Workflow Engine + Task Portal) with ASP.NET Core, React, SQL Server, and EF Core already configured.

Make sure your Part 2 application compiles and runs before starting this project.

Add SignalR and Dashboard Dependencies

Your workflow engine can start processes and complete tasks, but right now you have no way to see what is happening across all running workflows at once. Operations teams need live visibility into their systems.

In this step, you will add SignalR (a real-time communication library built into ASP.NET Core) and the Recharts charting library to your project. These two dependencies power the live dashboard you will build in the next steps.

In this step, get ready to:
  • Register SignalR services in the ASP.NET Core backend.
  • Install the SignalR JavaScript client and Recharts charting library.
  • Create a minimal SignalR hub and verify the app compiles.
Add SignalR services to Program.cs

SignalR is built into ASP.NET Core, so no NuGet package is needed on the server side. You just need to register the service and map a hub endpoint.

  • In your Program.cs file, find the section where you register services (where you see other builder.Services calls).
  • Add the SignalR service registration below your existing service registrations:
// Add SignalR real-time communication services
builder.Services.AddSignalR();
  • Now scroll down to where you configure the request pipeline (where you see app.MapControllers() or similar app.Map calls).
  • Add a using directive at the top of Program.cs so the file can find the hub class you will create shortly:
using BpmEngine.Hubs;
  • Add the hub endpoint mapping near your other app.Map calls:
// Map the SignalR hub endpoint for real-time dashboard communication
app.MapHub<WorkflowHub>("/workflowHub");

What do these lines do?

builder.Services.AddSignalR() registers SignalR's internal services (connection management, message serialization) into the dependency injection container.

app.MapHub<WorkflowHub>("/workflowHub") tells ASP.NET Core to listen for WebSocket connections at the /workflowHub path and route them to your hub class.

Install the React client packages

The React frontend needs a SignalR client library to connect to the hub, and Recharts to render live-updating charts.

  • Install the three npm packages by running this command in your React project directory (the folder containing your package.json):
npm install @microsoft/signalr@8.0.7 recharts@3.9.0 react-is

What are these packages?

  • @microsoft/signalr is the JavaScript client that connects to your ASP.NET Core SignalR hub over WebSockets.
  • recharts is a React charting library that renders bar charts, pie charts, and more as React components.
  • react-is is a peer dependency required by Recharts.
  • Confirm the install succeeded. You should see added 3 packages (or similar) in the terminal output with no errors.

Seeing permission errors or ERESOLVE conflicts?

  • Make sure you are running the command inside the React project folder (the one with package.json), not the root .NET project folder.
  • If you see a peer dependency conflict, try running the install with --legacy-peer-deps appended to the command.

Why won't my npm packages install?

Create a minimal WorkflowHub and verify

The Program.cs references a WorkflowHub class that does not exist yet. You need to create it before the app will compile.

  • Create a new folder called Hubs in your .NET project root (at the same level as your Controllers folder).
  • Inside the Hubs folder, create a new file called WorkflowHub.cs and add the following code:
using Microsoft.AspNetCore.SignalR;

namespace BpmEngine.Hubs;

public class WorkflowHub : Hub
{
}

What does this code do?

This is the simplest possible SignalR hub. It inherits from Hub which gives it built-in connection lifecycle management (clients can connect, disconnect, and join groups).

You will expand this class with custom methods in the next step. For now, the empty hub is enough to confirm your wiring is correct.

  • Save both Program.cs and Hubs/WorkflowHub.cs.
  • Build and run your .NET project to verify everything compiles by running:
dotnet build
  • You should see Build succeeded with 0 errors in the terminal output.

Seeing a build error?

  • If you see a "type or namespace 'WorkflowHub' could not be found" error, check that the using BpmEngine.Hubs; line is at the top of Program.cs and that your WorkflowHub.cs file uses the namespace BpmEngine.Hubs; declaration.
  • If you see a "Hubs does not exist" error, make sure the Hubs folder is inside your .NET project directory (the same folder as your .csproj file).

Help me fix my SignalR build error.

✔️ Awesome, I've got everything!

Great. Double check that you have saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the complete Hubs/WorkflowHub.cs file:

using Microsoft.AspNetCore.SignalR;

namespace BpmEngine.Hubs;

public class WorkflowHub : Hub
{
}

And confirm your Program.cs includes these two additions among your existing code:

// At the top of Program.cs:
using BpmEngine.Hubs;

// In the services section:
builder.Services.AddSignalR();

// In the endpoint mapping section:
app.MapHub<WorkflowHub>("/workflowHub");

Your backend is wired for real-time communication and your frontend has the charting tools it needs. Next up, you will build the audit log system and start broadcasting live workflow events through this hub.

Build the Audit Log and SignalR Broadcasting

Your SignalR hub is set up and the dashboard packages are installed. Now you need a way to record what happens inside the engine and push those events to connected clients.

In this step, you will create an audit log table that stores every workflow lifecycle event. You will also expand your SignalR hub to broadcast those events in real time to any connected dashboard.

In this step, get ready to:
  • Create an AuditLog entity and apply an EF Core migration.
  • Build an AuditService that logs workflow events and instrument your existing controllers.
  • Expand WorkflowHub with a strongly-typed client interface and real-time broadcasting.
Create the AuditLog entity and run the migration

Every operations dashboard needs historical data to chart. An audit log records each workflow event as an immutable row in the database.

  • Create a new folder called Models in your project (if it does not already exist).
  • Create a new file called AuditLog.cs inside the Models folder and add the following code:
namespace BpmEngine.Models;

// Represents a single audit event in the workflow lifecycle
public class AuditLog
{
    public int Id { get; set; }
    public int WorkflowInstanceId { get; set; }
    // Event types: InstanceStarted, TaskAssigned, TaskCompleted, InstanceCompleted
    public string EventType { get; set; } = string.Empty;
    public string UserId { get; set; } = string.Empty;
    public string Details { get; set; } = string.Empty;
    public DateTime Timestamp { get; set; }
}

What does this code do?

This entity maps to a database table that stores every event. Each row captures which workflow instance triggered the event, what kind of event it was, who caused it, and when it happened.

The EventType field uses strings like InstanceStarted, TaskAssigned, TaskCompleted, and InstanceCompleted. This follows the same pattern used in Cora SeQuence's audit log, where events are stored as static text.

  • Save Models/AuditLog.cs.

Now register this entity in your database context so EF Core knows about it.

  • Open your BpmDbContext.cs file. Find the section where your existing DbSet properties are declared (WorkflowDefinitions, WorkflowInstances, WorkflowTasks).
  • Add the following line alongside your other DbSet declarations:
public DbSet<AuditLog> AuditLogs { get; set; }

What does this line do?

Adding a DbSet<AuditLog> tells Entity Framework Core to create and manage an AuditLogs table in your SQL Server database. EF Core uses these properties to track which entities it manages.

  • Make sure you have a using BpmEngine.Models; statement at the top of your DbContext file if it is not already there.
  • Save BpmDbContext.cs.

Now generate and apply the migration to create the actual database table.

  • Run the migration scaffold command in your terminal:
dotnet ef migrations add AddAuditLog
  • Apply the migration to your database by running:
dotnet ef database update
  • You should see output confirming the migration was applied successfully. Look for a line mentioning AddAuditLog in the output.

Seeing a build error?

  • Make sure AuditLog.cs compiles without errors before running the migration. Check that the namespace matches your project.
  • If you see a "no DbContext was found" error, confirm that BpmDbContext.cs is in the correct project and is registered in Program.cs.

Help me troubleshoot my EF Core migration error for AddAuditLog

Create the AuditService and instrument your controllers

With the database table ready, you need a service class that controllers can call to log events. This keeps your logging logic in one place instead of duplicating it across every controller.

  • Create a folder called Services in your project if it does not already exist.
  • Create a new file called AuditService.cs inside the Services folder and add the following code:
using BpmEngine.Models;

namespace BpmEngine.Services;

public class AuditService
{
    private readonly BpmDbContext _context;

    public AuditService(BpmDbContext context)
    {
        _context = context;
    }

    // Logs a workflow event to the AuditLogs table
    public async Task LogEventAsync(int instanceId, string eventType, string userId, string details)
    {
        var entry = new AuditLog
        {
            WorkflowInstanceId = instanceId,
            EventType = eventType,
            UserId = userId,
            Details = details,
            Timestamp = DateTime.UtcNow
        };
        _context.AuditLogs.Add(entry);
        await _context.SaveChangesAsync();
    }
}

What does this code do?

The AuditService accepts the BpmDbContext through dependency injection. Its single method, LogEventAsync, creates a new AuditLog record and saves it immediately.

Controllers call this method after each action (starting an instance, assigning a task, completing a task). The timestamp is always UTC so events sort correctly regardless of time zone.

  • Save Services/AuditService.cs.
  • Register AuditService in your DI container. Open Program.cs and add this line near your other service registrations (before var app = builder.Build();):
builder.Services.AddScoped<AuditService>();
  • Save Program.cs.

Now instrument your existing workflow and task controllers to call the audit service after key actions.

  • Open your existing workflow controller (the one that handles starting instances).
  • Add AuditService as a constructor parameter alongside your existing dependencies:
// Add to your controller's constructor parameters
private readonly AuditService _auditService;

// In the constructor:
_auditService = auditService;
  • After the code that starts a workflow instance, add a call to log the event:
// After creating/starting a workflow instance:
await _auditService.LogEventAsync(instance.Id, "InstanceStarted", userId, $"Workflow instance {instance.Id} started");
  • In your task controller, add similar calls after task assignment and task completion:
// After assigning a task:
await _auditService.LogEventAsync(task.WorkflowInstanceId, "TaskAssigned", userId, $"Task {task.Id} assigned to {userId}");

// After completing a task:
await _auditService.LogEventAsync(task.WorkflowInstanceId, "TaskCompleted", userId, $"Task {task.Id} completed by {userId}");

What are these event types?

Each event type string represents a lifecycle moment. InstanceStarted fires when someone kicks off a workflow. TaskAssigned fires when a task is routed to a user. TaskCompleted fires when a user finishes their work item.

These map directly to the lifecycle events tracked in enterprise iBPMS platforms. The dashboard will chart them in real time.

  • Save your controller files.
  • Build the project to confirm everything compiles by running:
dotnet build
  • You should see "Build succeeded" with no errors in the terminal output.

Build failing with missing type errors?

  • Make sure your controllers have using BpmEngine.Services; at the top of the file.
  • Check that builder.Services.AddScoped<AuditService>(); is in Program.cs before var app = builder.Build();.
  • Verify that AuditService is added as a constructor parameter in the controller, not just a field declaration.

Help me fix build errors after adding AuditService to my BPM controllers

✔️ Awesome, I've got everything!

Great. Make sure you have saved both Services/AuditService.cs and your updated controller files.

ⓧ I'd like to double check the full code

Here is the complete Services/AuditService.cs file:

using BpmEngine.Models;

namespace BpmEngine.Services;

public class AuditService
{
    private readonly BpmDbContext _context;

    public AuditService(BpmDbContext context)
    {
        _context = context;
    }

    public async Task LogEventAsync(int instanceId, string eventType, string userId, string details)
    {
        var entry = new AuditLog
        {
            WorkflowInstanceId = instanceId,
            EventType = eventType,
            UserId = userId,
            Details = details,
            Timestamp = DateTime.UtcNow
        };
        _context.AuditLogs.Add(entry);
        await _context.SaveChangesAsync();
    }
}
Expand WorkflowHub with a typed client interface and broadcasting

Right now your hub is an empty class. SignalR supports strongly-typed hubs where client method names are checked at compile time. This prevents typos in method names from silently breaking real-time updates.

  • Open Hubs/WorkflowHub.cs and replace the entire file contents with the following code:
using Microsoft.AspNetCore.SignalR;

namespace BpmEngine.Hubs;

// Strongly-typed client interface for compile-time safety
public interface IDashboardClient
{
    Task ReceiveWorkflowEvent(WorkflowEventDto eventData);
    Task ReceiveMetricsUpdate(DashboardMetricsDto metrics);
}

public class WorkflowHub : Hub<IDashboardClient>
{
    private readonly BpmDbContext _context;

    public WorkflowHub(BpmDbContext context)
    {
        _context = context;
    }

    // Called by dashboard clients to join the broadcast group
    public async Task JoinDashboard()
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, "dashboard");
    }
}

// Data sent to clients for each workflow event
public class WorkflowEventDto
{
    public string EventType { get; set; } = string.Empty;
    public string Details { get; set; } = string.Empty;
    public DateTime Timestamp { get; set; }
    public int? WorkflowInstanceId { get; set; }
}

// Aggregated metrics sent to dashboard clients
public class DashboardMetricsDto
{
    public int ActiveInstances { get; set; }
    public int PendingTasks { get; set; }
    public int CompletedTasks { get; set; }
    public int CompletedLastHour { get; set; }
}

What does this code do?

  • IDashboardClient defines the two methods the server can call on connected clients: ReceiveWorkflowEvent for individual events and ReceiveMetricsUpdate for aggregated numbers.
  • Hub<IDashboardClient> replaces the plain Hub base class. Now the compiler ensures you only call methods that exist on the client interface.
  • JoinDashboard adds the caller to a SignalR group called dashboard. Broadcasting to a group means only dashboard pages receive the events, not every connected client.
  • WorkflowEventDto and DashboardMetricsDto are plain data classes that get serialized to JSON when sent over the WebSocket.
  • Save Hubs/WorkflowHub.cs.

Now inject the hub context into your controllers so they can broadcast events after logging them.

  • In your workflow and task controllers, add IHubContext<WorkflowHub, IDashboardClient> as a constructor parameter:
// Add to your controller's constructor parameters and field
private readonly IHubContext<WorkflowHub, IDashboardClient> _hubContext;

// In the constructor:
_hubContext = hubContext;
  • Add a using BpmEngine.Hubs; statement at the top of each controller file.
  • After each audit log call, broadcast the event to the dashboard group:
// After logging an event, broadcast to all dashboard clients:
await _hubContext.Clients.Group("dashboard").ReceiveWorkflowEvent(new WorkflowEventDto
{
    EventType = "InstanceStarted",
    Details = $"Workflow instance {instance.Id} started",
    Timestamp = DateTime.UtcNow,
    WorkflowInstanceId = instance.Id
});

Why IHubContext instead of the Hub directly?

You cannot inject WorkflowHub directly into a controller. Hubs are transient objects that only exist for the duration of a single client call.

IHubContext<WorkflowHub, IDashboardClient> gives you access to the hub's broadcasting capabilities from anywhere in your application. This is the recommended pattern for sending messages from outside a hub.

  • Save your controller files.
  • Build the project to confirm everything compiles by running:
dotnet build
  • You should see "Build succeeded" in the terminal output.

Seeing type errors for IDashboardClient or WorkflowEventDto?

  • Make sure your controllers have using BpmEngine.Hubs; at the top. The IDashboardClient, WorkflowEventDto, and DashboardMetricsDto types live in the BpmEngine.Hubs namespace.
  • Confirm that using Microsoft.AspNetCore.SignalR; is at the top of each controller that injects IHubContext.

Help me fix IHubContext injection errors in my ASP.NET Core BPM controller

✔️ Awesome, I've got everything!

Great. Make sure you have saved Hubs/WorkflowHub.cs and all your updated controller files.

ⓧ I'd like to double check the full code

Here is the complete Hubs/WorkflowHub.cs file for this step:

using Microsoft.AspNetCore.SignalR;

namespace BpmEngine.Hubs;

public interface IDashboardClient
{
    Task ReceiveWorkflowEvent(WorkflowEventDto eventData);
    Task ReceiveMetricsUpdate(DashboardMetricsDto metrics);
}

public class WorkflowHub : Hub<IDashboardClient>
{
    private readonly BpmDbContext _context;

    public WorkflowHub(BpmDbContext context)
    {
        _context = context;
    }

    public async Task JoinDashboard()
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, "dashboard");
    }
}

public class WorkflowEventDto
{
    public string EventType { get; set; } = string.Empty;
    public string Details { get; set; } = string.Empty;
    public DateTime Timestamp { get; set; }
    public int? WorkflowInstanceId { get; set; }
}

public class DashboardMetricsDto
{
    public int ActiveInstances { get; set; }
    public int PendingTasks { get; set; }
    public int CompletedTasks { get; set; }
    public int CompletedLastHour { get; set; }
}

Your audit log is recording events and your hub is ready to broadcast them. Next up, you will build the React dashboard that connects to this hub and renders live-updating charts.

Build the React Operations Dashboard

Your backend now records every workflow action in the audit log and broadcasts events through SignalR. The next piece is a frontend that actually consumes this data and displays it in real time.

In this step, you'll create an API endpoint that aggregates dashboard metrics from the database, then build a React component that connects to the SignalR hub and renders live-updating charts and an event feed.

In this step, get ready to:
  • Create an API endpoint that serves workflow metrics from the database.
  • Build a React dashboard component with a live SignalR connection.
  • Add Recharts visualizations and a real-time event feed panel.
Create the metrics API and connect to SignalR

The dashboard needs a source of truth for its initial state. When a user opens the operations page, it should immediately show current counts for running instances, pending tasks, and recent completions.

You'll create a dedicated controller that queries the database for these aggregates, then build a React component that fetches this data on load and subscribes to live updates via SignalR.

  • Create a new file called DashboardController.cs inside your Controllers folder.
  • Add the following code to define the metrics endpoint:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BpmEngine.Controllers;

[ApiController]
[Route("api/dashboard")]
public class DashboardController : ControllerBase
{
    private readonly BpmDbContext _context;

    public DashboardController(BpmDbContext context)
    {
        _context = context;
    }

    [HttpGet("metrics")]
    public async Task<IActionResult> GetMetrics()
    {
        var oneHourAgo = DateTime.UtcNow.AddHours(-1);

        // Count workflow instances currently in Running status
        var activeInstances = await _context.WorkflowInstances
            .CountAsync(i => i.Status == "Running");

        // Count tasks waiting to be picked up
        var pendingTasks = await _context.WorkflowTasks
            .CountAsync(t => t.Status == "Pending");

        // Count all completed tasks
        var completedTasks = await _context.WorkflowTasks
            .CountAsync(t => t.Status == "Completed");

        // Count completions in the last hour from audit log
        var completedLastHour = await _context.AuditLogs
            .CountAsync(a => a.EventType == "TaskCompleted" && a.Timestamp >= oneHourAgo);

        return Ok(new
        {
            activeInstances,
            pendingTasks,
            completedTasks,
            completedLastHour
        });
    }

    [HttpGet("auditlog/{instanceId}")]
    public async Task<IActionResult> GetAuditLog(int instanceId)
    {
        // Fetch audit entries for a specific workflow instance
        var logs = await _context.AuditLogs
            .Where(a => a.WorkflowInstanceId == instanceId)
            .OrderByDescending(a => a.Timestamp)
            .ToListAsync();

        return Ok(logs);
    }
}

What does this controller do?

The GetMetrics endpoint queries four counts from your database: active running instances, pending tasks, completed tasks, and tasks completed in the last hour. These four numbers power all the dashboard charts.

The GetAuditLog endpoint returns the full event history for a specific workflow instance, ordered newest first. This supports drill-down views later.

  • Save Controllers/DashboardController.cs.

Seeing a compile error?

Make sure your BpmDbContext class has DbSet<AuditLog> and DbSet<WorkflowInstance> and DbSet<WorkflowTask> properties declared. These were added in the previous steps.

Help me debug a compile error in my DashboardController

Now create the React component that will consume this endpoint and connect to SignalR for live updates.

  • Create a new file called Dashboard.jsx inside your src folder.
  • Add the following code to set up the SignalR connection and initial metrics fetch:
import { useState, useEffect, useRef } from "react";
import { HubConnectionBuilder } from "@microsoft/signalr";
import {
  BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip,
  PieChart, Pie, Cell,
  ResponsiveContainer
} from "recharts";

const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042"];

export default function Dashboard() {
  // Track live metrics from the backend
  const [metrics, setMetrics] = useState({
    activeInstances: 0,
    pendingTasks: 0,
    completedTasks: 0,
    completedLastHour: 0
  });
  const [events, setEvents] = useState([]);
  const [connected, setConnected] = useState(false);
  const connectionRef = useRef(null);

  useEffect(() => {
    // Build the SignalR connection to the workflow hub
    const connection = new HubConnectionBuilder()
      .withUrl("/workflowHub")
      .withAutomaticReconnect()
      .build();

    connectionRef.current = connection;

    // Listen for individual workflow events
    connection.on("ReceiveWorkflowEvent", (eventData) => {
      setEvents((prev) => [eventData, ...prev].slice(0, 50));
    });

    // Listen for updated aggregate metrics
    connection.on("ReceiveMetricsUpdate", (newMetrics) => {
      setMetrics(newMetrics);
    });

    // Start connection and join the dashboard broadcast group
    connection.start().then(() => {
      setConnected(true);
      connection.invoke("JoinDashboard");
    });

    connection.onclose(() => setConnected(false));
    connection.onreconnected(() => {
      setConnected(true);
      connection.invoke("JoinDashboard");
    });

    // Fetch initial metrics snapshot from the REST endpoint
    fetch("/api/dashboard/metrics")
      .then((res) => res.json())
      .then((data) => setMetrics(data));

    return () => { connection.stop(); };
  }, []);

  return (
    <div style={{ padding: "20px", fontFamily: "sans-serif" }}>
      <h1>Operations Dashboard</h1>
      <p style={{ color: connected ? "green" : "red" }}>
        {connected ? "Connected" : "Disconnected"}
      </p>
    </div>
  );
}

What's happening here?

HubConnectionBuilder creates a WebSocket connection to your /workflowHub endpoint. The withAutomaticReconnect() call ensures the client retries after network interruptions with default intervals of 0ms, 2s, 10s, and 30s.

After connecting, the client invokes JoinDashboard to join the SignalR group. This means the server can target broadcasts specifically to dashboard viewers. The component also re-joins on reconnection so it never misses events after a network blip.

  • Save src/Dashboard.jsx.
  • Run the app and confirm you see the "Operations Dashboard" heading with a green "Connected" status in the browser.

Seeing "Disconnected" in red?

Make sure your ASP.NET Core backend is running and that app.MapHub<WorkflowHub>("/workflowHub") is registered in Program.cs. Also verify the React dev server is proxying requests to the backend correctly.

Help me fix my SignalR connection issue

Add Recharts chart visualizations

Raw numbers are useful but charts make patterns visible at a glance. You'll add a bar chart comparing pending vs completed tasks and a pie chart showing active instances vs recent completions.

  • In src/Dashboard.jsx, find the closing }, []); of the useEffect block.
  • Add the following chart data derivation code between the useEffect and the return statement:
  const taskStatusData = [
    { name: "Pending", value: metrics.pendingTasks },
    { name: "Completed", value: metrics.completedTasks }
  ];

  const instanceData = [
    { name: "Active", value: metrics.activeInstances },
    { name: "Completed Last Hour", value: metrics.completedLastHour }
  ];

What does this code do?

These two arrays reshape the metrics state object into the format Recharts expects. Each chart takes an array of { name, value } objects. Because these are derived from state, the charts re-render automatically whenever new metrics arrive via SignalR.

  • Now find the closing </p> tag for the connection status paragraph inside the return statement.
  • Replace the closing </div> and everything after the </p> with the following chart grid:
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "20px" }}>
        <div>
          <h3>Tasks by Status</h3>
          <ResponsiveContainer width="100%" height={250}>
            <BarChart data={taskStatusData}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="name" />
              <YAxis />
              <Tooltip />
              <Bar dataKey="value" fill="#0088FE" />
            </BarChart>
          </ResponsiveContainer>
        </div>
        <div>
          <h3>Instances Overview</h3>
          <ResponsiveContainer width="100%" height={250}>
            <PieChart>
              <Pie data={instanceData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={80} label>
                {instanceData.map((entry, index) => (
                  <Cell key={entry.name} fill={COLORS[index % COLORS.length]} />
                ))}
              </Pie>
              <Tooltip />
            </PieChart>
          </ResponsiveContainer>
        </div>
      </div>
    </div>
  );

What do these charts show?

The BarChart displays a side-by-side comparison of pending versus completed tasks. At a glance, you can see whether your workflow engine is processing tasks faster than they arrive.

The PieChart shows the ratio of currently active workflow instances to those completed in the last hour. ResponsiveContainer ensures both charts resize gracefully within the grid.

  • Save src/Dashboard.jsx and refresh your browser.
  • You should see two chart areas rendered in a two-column grid. The bar chart shows "Pending" and "Completed" bars, and the pie chart shows "Active" and "Completed Last Hour" segments. Values will be zero until you run some workflows.

Charts not rendering?

Check that you installed react-is alongside recharts in Step 1. Recharts requires it as a peer dependency. Also confirm the taskStatusData and instanceData arrays are placed between the useEffect and the return statement, not inside either.

Help me fix my Recharts charts not appearing

Add a live event feed panel

Charts show aggregate metrics, but operations teams also need a real-time stream of individual events. When someone starts a workflow or completes a task, the event should appear instantly in the dashboard.

  • In src/Dashboard.jsx, find the closing </div> of the chart grid (the one right after the PieChart section).
  • Add the following live event feed section below it, before the final closing </div> of the component:
      <div style={{ marginTop: "30px" }}>
        <h3>Live Event Feed</h3>
        <div style={{ maxHeight: "300px", overflowY: "auto", border: "1px solid #ddd", padding: "10px" }}>
          {events.length === 0 && <p>No events yet. Start a workflow to see live updates.</p>}
          {events.map((evt, i) => (
            <div key={i} style={{ padding: "8px", borderBottom: "1px solid #eee", backgroundColor: i === 0 ? "#f0f8ff" : "transparent" }}>
              <strong>{evt.eventType}</strong> - {evt.details}
              <br />
              <small>{new Date(evt.timestamp).toLocaleTimeString()}</small>
            </div>
          ))}
        </div>
      </div>

What does the event feed do?

The ReceiveWorkflowEvent handler you registered earlier prepends each new event to the events array (capped at 50 entries). This feed renders them newest-first with a light blue highlight on the most recent entry so new activity catches your eye immediately.

Each event displays its type (like InstanceStarted or TaskCompleted), a details string, and a formatted timestamp. The scrollable container keeps the panel from growing unbounded.

  • Save src/Dashboard.jsx and refresh your browser.
  • You should see a "Live Event Feed" section below the charts with the message "No events yet. Start a workflow to see live updates." Once you trigger workflow actions through the Task Portal, events will appear here in real time.

Events not appearing in the feed?

Make sure your controllers are broadcasting to the "dashboard" group after each action. Check that the IHubContext<WorkflowHub, IDashboardClient> injection in your workflow and task controllers is calling Clients.Group("dashboard").ReceiveWorkflowEvent(...) after audit log entries.

Help me debug why SignalR events are not reaching my dashboard

✔️ Awesome, I've got everything!

Great. Double check you've saved both Controllers/DashboardController.cs and src/Dashboard.jsx.

ⓧ I'd like to double check the full code

Here is the complete Controllers/DashboardController.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BpmEngine.Controllers;

[ApiController]
[Route("api/dashboard")]
public class DashboardController : ControllerBase
{
    private readonly BpmDbContext _context;

    public DashboardController(BpmDbContext context)
    {
        _context = context;
    }

    [HttpGet("metrics")]
    public async Task<IActionResult> GetMetrics()
    {
        var oneHourAgo = DateTime.UtcNow.AddHours(-1);

        var activeInstances = await _context.WorkflowInstances
            .CountAsync(i => i.Status == "Running");

        var pendingTasks = await _context.WorkflowTasks
            .CountAsync(t => t.Status == "Pending");

        var completedTasks = await _context.WorkflowTasks
            .CountAsync(t => t.Status == "Completed");

        var completedLastHour = await _context.AuditLogs
            .CountAsync(a => a.EventType == "TaskCompleted" && a.Timestamp >= oneHourAgo);

        return Ok(new
        {
            activeInstances,
            pendingTasks,
            completedTasks,
            completedLastHour
        });
    }

    [HttpGet("auditlog/{instanceId}")]
    public async Task<IActionResult> GetAuditLog(int instanceId)
    {
        var logs = await _context.AuditLogs
            .Where(a => a.WorkflowInstanceId == instanceId)
            .OrderByDescending(a => a.Timestamp)
            .ToListAsync();

        return Ok(logs);
    }
}

Here is the complete src/Dashboard.jsx:

import { useState, useEffect, useRef } from "react";
import { HubConnectionBuilder } from "@microsoft/signalr";
import {
  BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip,
  PieChart, Pie, Cell,
  ResponsiveContainer
} from "recharts";

const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042"];

export default function Dashboard() {
  const [metrics, setMetrics] = useState({
    activeInstances: 0,
    pendingTasks: 0,
    completedTasks: 0,
    completedLastHour: 0
  });
  const [events, setEvents] = useState([]);
  const [connected, setConnected] = useState(false);
  const connectionRef = useRef(null);

  useEffect(() => {
    const connection = new HubConnectionBuilder()
      .withUrl("/workflowHub")
      .withAutomaticReconnect()
      .build();

    connectionRef.current = connection;

    connection.on("ReceiveWorkflowEvent", (eventData) => {
      setEvents((prev) => [eventData, ...prev].slice(0, 50));
    });

    connection.on("ReceiveMetricsUpdate", (newMetrics) => {
      setMetrics(newMetrics);
    });

    connection.start().then(() => {
      setConnected(true);
      connection.invoke("JoinDashboard");
    });

    connection.onclose(() => setConnected(false));
    connection.onreconnected(() => {
      setConnected(true);
      connection.invoke("JoinDashboard");
    });

    fetch("/api/dashboard/metrics")
      .then((res) => res.json())
      .then((data) => setMetrics(data));

    return () => { connection.stop(); };
  }, []);

  const taskStatusData = [
    { name: "Pending", value: metrics.pendingTasks },
    { name: "Completed", value: metrics.completedTasks }
  ];

  const instanceData = [
    { name: "Active", value: metrics.activeInstances },
    { name: "Completed Last Hour", value: metrics.completedLastHour }
  ];

  return (
    <div style={{ padding: "20px", fontFamily: "sans-serif" }}>
      <h1>Operations Dashboard</h1>
      <p style={{ color: connected ? "green" : "red" }}>
        {connected ? "Connected" : "Disconnected"}
      </p>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "20px" }}>
        <div>
          <h3>Tasks by Status</h3>
          <ResponsiveContainer width="100%" height={250}>
            <BarChart data={taskStatusData}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="name" />
              <YAxis />
              <Tooltip />
              <Bar dataKey="value" fill="#0088FE" />
            </BarChart>
          </ResponsiveContainer>
        </div>
        <div>
          <h3>Instances Overview</h3>
          <ResponsiveContainer width="100%" height={250}>
            <PieChart>
              <Pie data={instanceData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={80} label>
                {instanceData.map((entry, index) => (
                  <Cell key={entry.name} fill={COLORS[index % COLORS.length]} />
                ))}
              </Pie>
              <Tooltip />
            </PieChart>
          </ResponsiveContainer>
        </div>
      </div>
      <div style={{ marginTop: "30px" }}>
        <h3>Live Event Feed</h3>
        <div style={{ maxHeight: "300px", overflowY: "auto", border: "1px solid #ddd", padding: "10px" }}>
          {events.length === 0 && <p>No events yet. Start a workflow to see live updates.</p>}
          {events.map((evt, i) => (
            <div key={i} style={{ padding: "8px", borderBottom: "1px solid #eee", backgroundColor: i === 0 ? "#f0f8ff" : "transparent" }}>
              <strong>{evt.eventType}</strong> - {evt.details}
              <br />
              <small>{new Date(evt.timestamp).toLocaleTimeString()}</small>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

Your operations dashboard is live. It connects to the backend over WebSockets, displays aggregate metrics in charts, and streams individual events as they happen. Next up, you'll build the deployment bridge that lets your visual designer push workflow definitions directly into the engine.

Add Workflow Deployment from Designer JSON

Your operations dashboard can display live events, but right now there is no way to get new workflow designs into the engine. The visual designer from Part 1 exports JSON describing nodes and edges. The engine from Part 2 consumes WorkflowDefinition records stored in the database.

In this step, you will build the bridge between those two systems. A deployment endpoint accepts the designer's JSON export, validates the workflow structure, saves it as a new engine definition, and broadcasts the event to your live dashboard.

In this step, get ready to:
  • Create a deploy endpoint that accepts designer JSON and validates it.
  • Save valid workflows to the database and broadcast a deployment event via SignalR.
  • Test the full deployment flow from designer export to live dashboard notification.
Create the deploy controller with designer DTOs

The Part 1 visual designer exports a JSON object containing a name, a list of nodes (start, end, task, gateway), and a list of edges connecting them. You need C# classes that model this shape so ASP.NET Core can deserialize the incoming request body automatically.

  • Create a new file called Controllers/DeployController.cs in your project.
  • Add the following code, which defines the DTOs and the deploy endpoint:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using BpmEngine.Hubs;
using System.Text.Json;

namespace BpmEngine.Controllers;

[ApiController]
[Route("api/workflows")]
public class DeployController : ControllerBase
{
    private readonly BpmDbContext _context;
    private readonly IHubContext<WorkflowHub, IDashboardClient> _hubContext;

    public DeployController(BpmDbContext context, IHubContext<WorkflowHub, IDashboardClient> hubContext)
    {
        _context = context;
        _hubContext = hubContext;
    }

    [HttpPost("deploy")]
    public async Task<IActionResult> Deploy([FromBody] DesignerExportDto export)
    {
        // Validate that the request contains nodes and edges
        if (export.Nodes == null || export.Edges == null)
            return BadRequest(new { error = "Invalid workflow: nodes and edges are required" });

        // Check for required start and end nodes
        var hasStart = export.Nodes.Any(n => n.Type == "start");
        var hasEnd = export.Nodes.Any(n => n.Type == "end");

        if (!hasStart || !hasEnd)
            return BadRequest(new { error = "Workflow must have at least one start node and one end node" });

        // Create a new workflow definition from the designer export
        var definition = new WorkflowDefinition
        {
            Name = export.Name ?? "Untitled Workflow",
            DefinitionJson = JsonSerializer.Serialize(export),
            CreatedAt = DateTime.UtcNow
        };

        _context.WorkflowDefinitions.Add(definition);
        await _context.SaveChangesAsync();

        // Broadcast the deployment event to all connected dashboard clients
        await _hubContext.Clients.Group("dashboard").ReceiveWorkflowEvent(new WorkflowEventDto
        {
            EventType = "WorkflowDeployed",
            Details = $"Workflow '{definition.Name}' deployed (ID: {definition.Id})",
            Timestamp = DateTime.UtcNow
        });

        return Ok(new { id = definition.Id, name = definition.Name });
    }
}

// DTOs that model the visual designer's export format
public class DesignerExportDto
{
    public string? Name { get; set; }
    public List<DesignerNodeDto>? Nodes { get; set; }
    public List<DesignerEdgeDto>? Edges { get; set; }
}

public class DesignerNodeDto
{
    public string Id { get; set; } = string.Empty;
    public string Type { get; set; } = string.Empty;
    public string? Label { get; set; }
    public string? Role { get; set; }
}

public class DesignerEdgeDto
{
    public string Source { get; set; } = string.Empty;
    public string Target { get; set; } = string.Empty;
    public string? Label { get; set; }
}

What does this code do?

The bottom of the file defines three DTO classes that mirror the JSON your visual designer exports. DesignerExportDto wraps a workflow name, a list of nodes, and a list of edges.

The Deploy method runs two validation checks: it rejects requests missing nodes or edges, and it rejects workflows that lack a start or end node. These checks prevent broken workflows from reaching the engine.

On success, the method serializes the entire export into DefinitionJson and saves a new WorkflowDefinition record. It then broadcasts a WorkflowDeployed event to every connected dashboard client.

  • Save Controllers/DeployController.cs.
  • Confirm the project compiles by running:
dotnet build

You should see a successful build with no errors.

Seeing a build error?

  • If you see an error about WorkflowDefinition not being found, make sure your existing model class from Part 2 includes Name, DefinitionJson, and CreatedAt properties.
  • If WorkflowEventDto or IDashboardClient cannot be resolved, check that the using BpmEngine.Hubs; statement is at the top of the file.

help me fix my DeployController build error

Understand the IHubContext broadcasting pattern

The controller injects IHubContext<WorkflowHub, IDashboardClient> instead of the hub itself. This is the ASP.NET Core pattern for broadcasting SignalR messages from outside a hub class.

Why IHubContext instead of the Hub directly?

A Hub instance only exists for the duration of a single client call. Controllers, background services, and other classes cannot instantiate a Hub directly.

IHubContext gives you access to the same Clients and Groups APIs without needing a live hub connection. ASP.NET Core registers it automatically when you call AddSignalR(), so you just request it via constructor injection.

Because the controller uses the strongly-typed IDashboardClient interface, the broadcast call is ReceiveWorkflowEvent(...) with compile-time safety. If you misspelled the method name, the compiler would catch it before runtime.

Test the deployment flow end to end

With the endpoint built, you can test the full deployment pipeline. You will POST a sample designer JSON payload to the deploy endpoint and verify both the database save and the live dashboard notification.

  • Make sure the application is running:
dotnet run
  • Open a new terminal and send a test deployment request by running:
curl -X POST http://localhost:5000/api/workflows/deploy -H "Content-Type: application/json" -d "{\"name\": \"Leave Approval\", \"nodes\": [{\"id\": \"1\", \"type\": \"start\", \"label\": \"Start\"}, {\"id\": \"2\", \"type\": \"task\", \"label\": \"Review\", \"role\": \"manager\"}, {\"id\": \"3\", \"type\": \"end\", \"label\": \"End\"}], \"edges\": [{\"source\": \"1\", \"target\": \"2\"}, {\"source\": \"2\", \"target\": \"3\"}]}"

What does this test payload represent?

This JSON mimics what your Part 1 visual designer exports: a simple "Leave Approval" workflow with a start node, one task node assigned to a manager role, and an end node. The edges connect them in sequence.

If you have your actual Part 1 designer running, you can also export real workflow JSON from the canvas and POST that instead.

  • Verify the response contains an id and name field confirming the workflow was saved.
  • Switch to your browser and open the Operations dashboard.
  • Check the Live Event Feed panel. You should see a new event with type WorkflowDeployed and details showing the workflow name and ID.

Don't see the event in the dashboard?

  • Make sure the dashboard page was open before you sent the deploy request. The SignalR connection must be active to receive broadcast events.
  • Check that the dashboard shows "Connected" in green. If it shows "Disconnected", refresh the page and try the curl command again.
  • If the curl command returns a 404, verify your app is running on port 5000. Adjust the URL if your launch settings use a different port.

help me debug why my deploy endpoint isn't broadcasting to the dashboard

  • Test the validation by sending an invalid payload (missing the end node) and confirming you receive a 400 error:
curl -X POST http://localhost:5000/api/workflows/deploy -H "Content-Type: application/json" -d "{\"name\": \"Bad Workflow\", \"nodes\": [{\"id\": \"1\", \"type\": \"start\"}], \"edges\": []}"

You should see a response containing the error message about needing at least one start node and one end node.

✔️ Awesome, I've got everything!

Great. Double check you have saved Controllers/DeployController.cs and that your build is clean.

ⓧ I'd like to double check the full code

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using BpmEngine.Hubs;
using System.Text.Json;

namespace BpmEngine.Controllers;

[ApiController]
[Route("api/workflows")]
public class DeployController : ControllerBase
{
    private readonly BpmDbContext _context;
    private readonly IHubContext<WorkflowHub, IDashboardClient> _hubContext;

    public DeployController(BpmDbContext context, IHubContext<WorkflowHub, IDashboardClient> hubContext)
    {
        _context = context;
        _hubContext = hubContext;
    }

    [HttpPost("deploy")]
    public async Task<IActionResult> Deploy([FromBody] DesignerExportDto export)
    {
        if (export.Nodes == null || export.Edges == null)
            return BadRequest(new { error = "Invalid workflow: nodes and edges are required" });

        var hasStart = export.Nodes.Any(n => n.Type == "start");
        var hasEnd = export.Nodes.Any(n => n.Type == "end");

        if (!hasStart || !hasEnd)
            return BadRequest(new { error = "Workflow must have at least one start node and one end node" });

        var definition = new WorkflowDefinition
        {
            Name = export.Name ?? "Untitled Workflow",
            DefinitionJson = JsonSerializer.Serialize(export),
            CreatedAt = DateTime.UtcNow
        };

        _context.WorkflowDefinitions.Add(definition);
        await _context.SaveChangesAsync();

        await _hubContext.Clients.Group("dashboard").ReceiveWorkflowEvent(new WorkflowEventDto
        {
            EventType = "WorkflowDeployed",
            Details = $"Workflow '{definition.Name}' deployed (ID: {definition.Id})",
            Timestamp = DateTime.UtcNow
        });

        return Ok(new { id = definition.Id, name = definition.Name });
    }
}

public class DesignerExportDto
{
    public string? Name { get; set; }
    public List<DesignerNodeDto>? Nodes { get; set; }
    public List<DesignerEdgeDto>? Edges { get; set; }
}

public class DesignerNodeDto
{
    public string Id { get; set; } = string.Empty;
    public string Type { get; set; } = string.Empty;
    public string? Label { get; set; }
    public string? Role { get; set; }
}

public class DesignerEdgeDto
{
    public string Source { get; set; } = string.Empty;
    public string Target { get; set; } = string.Empty;
    public string? Label { get; set; }
}

Your deployment bridge is live. Next up, you will unify the Designer, Task Portal, and Operations Dashboard into a single navigable application with a shared shell.

Unify All Three Projects into One Platform

Your deployment endpoint now bridges the visual designer to the running engine. The final piece is bringing everything together into a single, navigable application.

Right now, your three projects (Designer, Task Portal, Operations Dashboard) exist as separate components with no way to switch between them. In this step, you will create a unified navigation shell that ties all three into one platform you can run with a single command.

In this step, get ready to:
  • Create a navigation shell that switches between Designer, Task Portal, and Operations pages.
  • Add a Deploy to Engine button in the Designer that sends workflow JSON to the backend.
  • Verify the full end-to-end flow across all three parts of the platform.
Create the App.jsx navigation shell

Enterprise platforms like Cora SeQuence present their designer, task portal, and operations dashboard as pages within a single navigation shell. You will replicate this pattern using simple state-based page switching in React.

  • Open src/App.jsx in your editor.
  • Replace the entire contents of the file with the following code:
import { useState } from "react";
import Dashboard from "./Dashboard";
import WorkflowDesigner from "./WorkflowDesigner";
import TaskPortal from "./TaskPortal";

// Define the three pages available in the platform
const NAV_ITEMS = [
  { key: "designer", label: "Designer" },
  { key: "tasks", label: "Task Portal" },
  { key: "operations", label: "Operations" }
];

export default function App() {
  // Track which page is currently active
  const [activePage, setActivePage] = useState("operations");

  return (
    <div>
      <nav style={{
        display: "flex",
        gap: "0",
        backgroundColor: "#1a1a2e",
        padding: "0 20px"
      }}>
        <h2 style={{ color: "#fff", margin: "0", padding: "12px 20px 12px 0" }}>
          iBPMS Platform
        </h2>
        {NAV_ITEMS.map((item) => (
          <button
            key={item.key}
            onClick={() => setActivePage(item.key)}
            style={{
              padding: "12px 20px",
              border: "none",
              backgroundColor: activePage === item.key ? "#16213e" : "transparent",
              color: activePage === item.key ? "#4fc3f7" : "#ccc",
              cursor: "pointer",
              fontSize: "14px",
              fontWeight: activePage === item.key ? "bold" : "normal",
              borderBottom: activePage === item.key ? "2px solid #4fc3f7" : "2px solid transparent"
            }}
          >
            {item.label}
          </button>
        ))}
      </nav>

      <main>
        {activePage === "designer" && <WorkflowDesigner />}
        {activePage === "tasks" && <TaskPortal />}
        {activePage === "operations" && <Dashboard />}
      </main>
    </div>
  );
}

What does this code do?

The NAV_ITEMS array defines the three pages in your platform. Each item has a key (used for state matching) and a label (displayed in the nav bar).

The activePage state variable controls which component renders in the <main> section. Clicking a nav button updates the state, which conditionally renders the matching component. No routing library is needed for this simple page-switching pattern.

  • Save src/App.jsx.
  • Refresh your browser. You should see a dark navigation bar at the top with "iBPMS Platform" on the left and three buttons: Designer, Task Portal, and Operations.
  • Click each nav button to confirm the page switches between your Designer, Task Portal, and Operations Dashboard components.

Don't see the nav bar or pages?

Make sure your imports at the top of App.jsx match the actual filenames of your existing components. If your designer file is named differently (e.g. Designer.jsx instead of WorkflowDesigner.jsx), update the import path to match.

Check the browser console for errors. A failed import will show a module-not-found error naming the missing file.

help me debug why my App.jsx navigation shell isn't rendering

Add a Deploy to Engine button in the Designer

The navigation shell lets you switch between pages, but the Designer and Engine are still disconnected. You need a way to send a workflow designed in Part 1 directly to the engine's deploy endpoint without leaving the platform.

  • Open your WorkflowDesigner.jsx component (from Part 1).
  • Add a deploy handler function inside your component that takes the current canvas export JSON and POSTs it to the deploy endpoint. Add the following function inside your component, above the return statement:
// Deploy the current workflow design to the BPM engine
const handleDeploy = async () => {
  // Get the current canvas state (nodes and edges from your designer)
  const exportData = {
    name: "My Workflow",
    nodes: nodes,
    edges: edges
  };

  try {
    const response = await fetch("/api/workflows/deploy", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(exportData)
    });

    if (response.ok) {
      const result = await response.json();
      alert(`Workflow deployed successfully! ID: ${result.id}`);
    } else {
      const error = await response.json();
      alert(`Deploy failed: ${error.error}`);
    }
  } catch (err) {
    alert("Deploy failed: Could not reach the server.");
  }
};

What does this code do?

The handleDeploy function gathers the current nodes and edges state from your designer canvas and sends them to /api/workflows/deploy as JSON.

The deploy endpoint (from Step 4) validates the workflow has start and end nodes, saves it to the database, and broadcasts a WorkflowDeployed event to all connected dashboard clients via SignalR.

  • Add a Deploy button in your Designer's JSX return block. Place it near your existing toolbar or export button:
<button onClick={handleDeploy} style={{ padding: "8px 16px", backgroundColor: "#4CAF50", color: "white", border: "none", borderRadius: "4px", cursor: "pointer" }}>
  Deploy to Engine
</button>

What does this button do?

When clicked, this button triggers handleDeploy. On success, the learner sees an alert with the new workflow definition ID. On failure, they see the validation error from the server (e.g. missing start or end node).

  • Save WorkflowDesigner.jsx.
  • Refresh your browser and click Designer in the nav bar. You should see the green Deploy to Engine button in your designer toolbar.

Deploy button not appearing?

Confirm the button JSX is inside the component's return block, not outside it. The handleDeploy function goes above the return, and the <button> element goes inside the return.

help me figure out where to place the Deploy button in my WorkflowDesigner component

Test the complete end-to-end flow

With all three parts connected through a shared backend and database, you can now verify the entire platform works as one cohesive application.

  • Make sure your .NET backend is running. If not, start it by running:
dotnet run
  • In the browser, click Designer in the nav bar.
  • Create a simple workflow with at least a start node, one task node, and an end node. Connect them with edges.
  • Click Deploy to Engine. You should see an alert confirming the workflow was deployed with a new definition ID.
  • Click Operations in the nav bar. You should see a "WorkflowDeployed" event in the Live Event Feed showing the name and ID of your new workflow.
  • Click Task Portal in the nav bar. Start an instance of your newly deployed workflow, then assign and complete a task.
  • Switch back to Operations. You should see new events in the Live Event Feed (InstanceStarted, TaskAssigned, TaskCompleted) streaming in real time without refreshing the page.

What just happened?

You just ran the full iBPMS lifecycle in one application. The Designer produced JSON that the deploy endpoint saved as a WorkflowDefinition. The Task Portal started an instance of that definition and routed tasks to users. The Operations Dashboard received every event via SignalR in real time.

All three parts share the same database and backend API. The SignalR hub is the communication bridge that makes the dashboard "live" without polling.

Deploy succeeds but no event appears in Operations?

Check the connection status indicator on the Operations page. If it shows "Disconnected", the SignalR connection dropped. Refresh the page to reconnect, then try deploying again.

If the status is "Connected" but events still don't appear, verify that your DeployController is broadcasting to the "dashboard" group. The group name must match exactly between JoinDashboard in the hub and Clients.Group("dashboard") in the controller.

help me debug why my SignalR events aren't reaching the Operations dashboard after deploy

✔️ Awesome, I've got everything!

Great. Double check you have saved both src/App.jsx and src/WorkflowDesigner.jsx.

ⓧ I'd like to double check the full code

Here is the complete src/App.jsx file:

import { useState } from "react";
import Dashboard from "./Dashboard";
import WorkflowDesigner from "./WorkflowDesigner";
import TaskPortal from "./TaskPortal";

const NAV_ITEMS = [
  { key: "designer", label: "Designer" },
  { key: "tasks", label: "Task Portal" },
  { key: "operations", label: "Operations" }
];

export default function App() {
  const [activePage, setActivePage] = useState("operations");

  return (
    <div>
      <nav style={{ display: "flex", gap: "0", backgroundColor: "#1a1a2e", padding: "0 20px" }}>
        <h2 style={{ color: "#fff", margin: "0", padding: "12px 20px 12px 0" }}>iBPMS Platform</h2>
        {NAV_ITEMS.map((item) => (
          <button key={item.key} onClick={() => setActivePage(item.key)} style={{ padding: "12px 20px", border: "none", backgroundColor: activePage === item.key ? "#16213e" : "transparent", color: activePage === item.key ? "#4fc3f7" : "#ccc", cursor: "pointer", fontSize: "14px", fontWeight: activePage === item.key ? "bold" : "normal", borderBottom: activePage === item.key ? "2px solid #4fc3f7" : "2px solid transparent" }}>{item.label}</button>
        ))}
      </nav>
      <main>
        {activePage === "designer" && <WorkflowDesigner />}
        {activePage === "tasks" && <TaskPortal />}
        {activePage === "operations" && <Dashboard />}
      </main>
    </div>
  );
}

Secret mission

Real-Time Task Reallocation

Enterprise operations managers need more than read-only dashboards. They need to act on what they see. In this mission, you'll add bidirectional SignalR communication so managers can reassign tasks directly from the Operations Dashboard, with every connected client seeing the change instantly.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally using SQL Server LocalDB and your file system, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The iBPMS platform project directory (contains all .NET backend and React frontend code from Parts 1-3).
  • SQL Server LocalDB database (stores workflow definitions, instances, tasks, and audit logs).
  • EF Core migrations (including the AddAuditLog migration added in this project).
  • npm packages installed in the React project (@microsoft/signalr, recharts, react-is).

Keep everything running

No action needed. Choose this if you are still actively building on the platform or plan to continue with future extensions.

  • Your .NET backend and React dev server will stop when you close the terminal, but all code and database data remain intact.
  • Next time you want to work on the project, just run dotnet run and npm start again from the project directory.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your project files and database stay on disk for when you return.

  • Press Ctrl+C in the terminal running your .NET backend to stop the server.
  • Press Ctrl+C in the terminal running the React dev server to stop it.
  • Close any open browser tabs for the application.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your code, database, and all workflow data permanently.

  • Stop any running servers by pressing Ctrl+C in each terminal.
  • Drop the SQL Server database by running this command from your project directory:
dotnet ef database drop --force
  • Delete the project folder. On macOS or Linux, run:
cd ~/Desktop
rm -rf BpmEngine
  • On Windows, run:
cd ~\Desktop
rmdir /s /q BpmEngine

What about the npm packages?

The @microsoft/signalr, recharts, and react-is packages live inside the project's node_modules folder. Deleting the project directory removes them automatically.

Nice Work!

Nice Work!

You just built a complete, unified iBPMS platform with real-time operations monitoring, audit logging, workflow deployment, and a navigation shell connecting every piece together.

You've learned how to:

  • Build a SignalR hub that broadcasts workflow lifecycle events to connected clients in real time.
  • Create an audit log system that records every workflow action with timestamps in SQL Server.
  • Build a React operations dashboard with live-updating Recharts charts showing workflow metrics.
  • Unify all three projects (Designer, Engine + Task Portal, Operations Dashboard) into one runnable platform with a shared navigation shell.
  • Secret Mission: Implemented real-time task reallocation with bidirectional SignalR communication and optimistic UI updates.

Ready to quiz yourself?