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 |
Related Issues¶
| 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 |