Skip to content

Mercury Relay

Mercury Relay is a SignalR-based system for managing pharmacy computers remotely. It allows the support team to see which devices are online, assign printer roles, and push configuration changes in real time.

Overview

Each pharmacy (customer) has multiple computers running the Medicine desktop application. One computer is typically connected to a label printer via USB. Other computers on the local network print through that computer over TCP. The relay system lets admins manage these assignments from the Portal.Web admin panel without touching the physical machines.

graph LR
    subgraph Internet
        GW[YARP Gateway]
    end

    subgraph Mercury Cluster
        Portal[Portal.Web<br/>Blazor Server]
        Store[Store.Api]
        Identity[Identity.Api]
    end

    subgraph Pharmacy["Pharmacy (on-site)"]
        M1[Medicine Client 1<br/>PrinterComputer]
        M2[Medicine Client 2]
        M3[Medicine Client 3]
        Printer[Label Printer]
    end

    Portal -->|REST via Gateway| Store
    Store -->|Internal API| Identity
    M1 & M2 & M3 -->|SignalR| Portal
    M1 -->|USB| Printer
    M2 & M3 -->|TCP :9100| M1

Service Architecture

Mercury is a multi-service system. The relay feature spans two of these services:

Service Role in Relay
Portal.Web Blazor Server app. Hosts the SignalR hub that desktop clients connect to. Renders the admin panel UI.
Store.Api REST API. Stores customer and device data. Serves relay endpoints for the admin panel.
Identity.Api Authentication. Not directly involved in relay, but handles JWT tokens and internal API auth.
Mercury.Gateway YARP reverse proxy. Routes /Store/* to Store.Api. Strips X-Internal-Key header (see Internal API).
Mercury.Core Shared library. Contains IRelayHub, IRelayClient interfaces and relay models.
graph TB
    subgraph Portal.Web
        Hub[RelayHub<br/>SignalR]
        UI[Admin Panel<br/>Blazor Pages]
    end

    subgraph Store.Api
        RC[RelayController]
        DB[(Database)]
    end

    subgraph Core[Mercury.Core]
        IH[IRelayHub]
        IC[IRelayClient]
        Models[DeviceRegistration<br/>DeviceIdentity<br/>DeviceRole<br/>DeviceStatus<br/>NudgeResponse]
    end

    subgraph Medicine[Medicine Desktop]
        Client[SignalR Client]
    end

    Client -->|implements IRelayClient| Hub
    Hub -->|implements IRelayHub| Client
    UI -->|HTTP via Gateway| RC
    RC --> DB
    Hub -.->|references| IH & IC & Models
    RC -.->|references| Models

Device Roles

Each computer in a pharmacy has one of three roles:

Role Description
PrinterComputer Physically connected to the label printer via USB. Exposes a TCP port (default 9100) for network printing. Knows its printer name.
Client Prints over the network. Connects to a PrinterComputer's TCP port. Knows which PrinterComputer to target.
None Not part of the printing configuration.
graph LR
    subgraph Pharmacy
        PC1["KASA-01<br/>Role: PrinterComputer<br/>Printer: Zebra ZD421<br/>Port: 9100"]
        PC2["KASA-02<br/>Role: Client<br/>Target: KASA-01:9100"]
        PC3["DEPO-01<br/>Role: Client<br/>Target: KASA-01:9100"]
        PC4["OFIS-01<br/>Role: None"]
        LBL[Label Printer<br/>Zebra ZD421]
    end

    PC1 -->|USB| LBL
    PC2 -->|TCP :9100| PC1
    PC3 -->|TCP :9100| PC1

SignalR Hub Interfaces

The hub contract is defined in Mercury.Core so both the server (Portal.Web) and future client (Medicine desktop) reference the same types.

IRelayHub (server methods — client calls these)

Method Parameters Returns Purpose
RegisterDeviceAsync DeviceRegistration DeviceIdentity Register on connect. Server returns assigned device key and connection info.
UnregisterDeviceAsync Clean disconnect.
RelayMessageAsync targetDeviceKey, byte[] payload Send arbitrary data to another device in the same pharmacy.
NudgeAsync targetDeviceKey NudgeResponse Ping a device. Returns alive status and latency.
UpdateRoleAsync DeviceRole Device self-reports its role.

IRelayClient (client callbacks — server calls these)

Method Parameters Returns Purpose
ReceiveMessageAsync senderDeviceKey, byte[] payload Receive relayed data from another device.
RoleChangedAsync DeviceRole Server pushes a role change (admin changed it from the panel).
NudgeRequestAsync NudgeResponse Server pings the client. Client responds with alive + latency.
DeviceStatusChangedAsync deviceKey, DeviceStatus Notification that another device in the pharmacy went online/offline.

Models

classDiagram
    class DeviceRegistration {
        +string ComputerName
        +string Username
        +string LocalIpAddress
        +string Email
        +string ClientVersion
        +int PharmacyId
        +DeviceRole Role
    }

    class DeviceIdentity {
        +string DeviceKey
        +string ConnectionId
        +string PublicIpAddress
    }

    class NudgeResponse {
        +bool IsAlive
        +long LatencyMs
        +DateTime Timestamp
    }

    class DeviceRole {
        <<enumeration>>
        None
        Server
        Client
    }

    class DeviceStatus {
        <<enumeration>>
        Online
        Offline
    }

    DeviceRegistration --> DeviceRole
    DeviceIdentity ..> DeviceRegistration : returned after register

Data Model

Store.Api persists device and configuration data:

erDiagram
    Customer ||--o{ RelayComputer : "has"
    Customer {
        int Id PK
        string Name
        string Email
    }

    RelayComputer {
        int Id PK
        int CustomerId FK
        string PcName
        string Username
        string LocalIpAddress
        string SoftwareVersion
        bool IsOnline
        datetime LastOnlineAt
        int Role "0=None 1=PrinterComputer 2=Client"
        string PrinterName "nullable"
        int TcpPort "nullable"
        string PrinterComputerName "nullable"
        int TotalRamMb "nullable"
        string SupportAppNumber "nullable"
    }

    Customer ||--o| PrinterConfig : "has active"
    PrinterConfig {
        int Id PK
        int CustomerId FK
        int PrinterComputerId FK
        string PrinterName
        int TcpPort
        datetime UpdatedAt
    }

Admin Panel

The relay admin panel is at /relay in Portal.Web, visible to admin, support, and super_support roles.

Customer List (/relay)

Shows all pharmacies with connected device counts. Filterable by store ID, name, email, phone. Click a row to open the detail page.

Customer Detail (/relay/{storeId})

Shows all computers for a pharmacy with their online status, role, software version, RAM, and printer configuration. Admins can change roles and assign printers from this page.

Printer Assignment Flow

When an admin changes a computer's role to PrinterComputer, the system needs to query that computer for its available printers before saving the configuration.

sequenceDiagram
    actor Admin
    participant Panel as Admin Panel
    participant API as Store.Api
    participant Hub as SignalR Hub
    participant NewPC as Target PC
    participant OldPC as Old PrinterPC
    participant Clients as Other Clients

    Admin->>Panel: Change DEPO-01 role to PrinterComputer
    Panel->>API: POST /relay/customers/{id}/request-printers
    API->>Hub: RequestPrinterList(DEPO-01)
    Hub->>NewPC: List your connected printers

    Note over NewPC: 2-5 sec delay<br/>(WMI/system query)

    NewPC->>Hub: ["Zebra ZD421", "Xprinter XP-370B"]
    Hub->>API: Printer list received
    API->>Panel: Show printer dropdown

    Admin->>Panel: Select "Zebra ZD421", port 9100, save

    Panel->>API: POST /relay/customers/{id}/printer-config
    API->>API: Persist PrinterConfig

    par Push to all online devices
        Hub->>NewPC: Role: PrinterComputer, Printer: Zebra ZD421, Port: 9100
        Hub->>OldPC: Role: Client, Target: DEPO-01:9100
        Hub->>Clients: Target: DEPO-01:9100
    end

    Note over Clients: All clients now print<br/>through DEPO-01

Role Change Rules

From To Behavior
None / Client PrinterComputer Queries device for printer list (async, with loading spinner). Old PrinterComputer demoted to Client if online.
None Client Immediate. Auto-assigns to existing PrinterComputer.
PrinterComputer Client Immediate. Assigns to remaining PrinterComputer if one exists.
Any None Immediate. Clears printer configuration.

Device Connection Lifecycle

sequenceDiagram
    participant Med as Medicine Desktop
    participant Hub as RelayHub (Portal.Web)
    participant DB as Store.Api (DB)

    Med->>Hub: Connect (SignalR WebSocket)
    Med->>Hub: RegisterDeviceAsync(registration)

    Note over Hub: Generate DeviceKey<br/>(computerName + username)

    Hub->>DB: Upsert RelayComputer (IsOnline=true)
    Hub-->>Med: DeviceIdentity (deviceKey, connectionId)

    Hub->>Hub: Notify other devices in pharmacy
    Hub-->>Med: Current PrinterConfig (if exists)

    loop Heartbeat
        Hub->>Med: NudgeRequestAsync()
        Med-->>Hub: NudgeResponse (isAlive, latencyMs)
    end

    Med->>Hub: Disconnect
    Hub->>DB: Update IsOnline=false, LastOnlineAt=now
    Hub->>Hub: Notify other devices: DeviceStatusChanged(offline)

API Endpoints

All relay endpoints are on Store.Api under /api/v1/panel/relay. Accessed by Portal.Web through the YARP gateway at /Store/api/v1/panel/relay/*.

Method Path Auth Purpose
GET /customers Admin, Support, SuperSupport List all pharmacies with device counts
GET /customers/{customerId} Admin, Support, SuperSupport Detail view with all computers
POST /customers/{customerId}/request-printers Admin Request printer list from a device (planned)
POST /customers/{customerId}/printer-config Admin Save printer assignment and push to devices (planned)

Request Flow (Admin Panel to API)

flowchart LR
    Browser -->|Blazor Server<br/>no HTTP| Portal.Web
    Portal.Web -->|"GET /Store/api/v1/panel/relay/customers"| Gateway
    Gateway -->|"strips /Store prefix<br/>strips X-Internal-Key"| Store.Api
    Store.Api -->|query| DB[(Database)]
    DB -->|results| Store.Api
    Store.Api -->|JSON| Gateway
    Gateway -->|JSON| Portal.Web
    Portal.Web -->|render HTML| Browser

Note

Portal.Web is a Blazor Server app. The browser does not make direct HTTP calls to the API. All API requests originate from the server-side Blazor process through the YARP gateway.

Current Status

Component Status
Hub interfaces (IRelayHub, IRelayClient) Defined in Mercury.Core and Portal.Web
Models (DeviceRegistration, DeviceIdentity, etc.) Defined in Mercury.Core and Portal.Web
Customer list API (GET /customers) Live
Customer detail API (GET /customers/{id}) Live
Admin panel — customer list page Live
Admin panel — device detail page Live (mock role editing)
Printer assignment flow Frontend mock only
SignalR hub implementation Not started
Device registry persistence Not started
Shared contracts for Medicine desktop Deferred

Implementation Roadmap

gantt
    title Mercury Relay Implementation
    dateFormat YYYY-MM-DD
    section Done
        Portal.Web SignalR cleanup          :done, d1, 2026-02-20, 3d
        Hub interfaces and models           :done, d2, 2026-03-01, 1d
        Move relay panel to main navbar     :done, d3, 2026-03-01, 1d
        Customer list and detail APIs       :done, d4, 2026-02-25, 3d
    section In Progress
        Admin panel role editing (mock)     :active, a1, 2026-03-02, 5d
    section Planned
        Client test page in Portal.Web     :p1, after a1, 3d
        RelayHub implementation            :p2, after p1, 5d
        Device registry persistence        :p3, after p2, 3d
        Admin panel device management      :p4, after p3, 5d
        Wire panel to real SignalR data    :p5, after p4, 3d
        Shared contracts for Medicine      :p6, after p5, 5d

Source Files

File Purpose
Mercury.Core/Services/Relay/IRelayHub.cs Server hub interface
Mercury.Core/Services/Relay/IRelayClient.cs Client callback interface
Mercury.Core/Services/Relay/Models/* Shared models (DeviceRegistration, DeviceIdentity, etc.)
Portal.Web/Hubs/Relay/* Duplicate interfaces in Portal.Web (for Blazor Server reference)
Portal.Web/Pages/Relay/RelayPage.razor Customer list page
Portal.Web/Pages/Relay/RelayDetailPage.razor Device detail and printer config page
Portal.Web/Requests/Apis/Store/Relay/StoreRelayRequest.cs API client for relay endpoints
Store.Api/Managements/Panel/Relay/Controllers/RelayController.cs REST controller
Store.Api/Managements/Panel/Relay/Commands/* MediatR command handlers
Store.Api/Managements/Panel/Relay/Dtos/* Response DTOs
Issue Title Status
#412 Mercury Relay (epic) Open
#429 Shared contracts (Mercury.Relay.Contracts) Deferred
#430 RelayHub implementation Not started
#431 Device registry persistence Not started
#432 Admin panel device management Not started
#433 Portal.Web SignalR cleanup Done
#451 Support panel UI In progress
#454 Hub interfaces and models in Portal.Web Done