A local NOC dashboard for checking and controlling a driveway gate via a Remootio smart gate controller connected to a Ghost Controls gate system.
The repo ships two equivalent front-end implementations and one shared .NET library:
| Project | Stack | Description |
|---|---|---|
angular/ |
Angular 22 + RxJS | Original dashboard — runs in the browser, connects to the Remootio device WebSocket directly |
dotnet/GateMonitor.Blazor |
Blazor WebAssembly (.NET 10) | Equivalent SPA — same direct WebSocket approach, orchestrated by Aspire |
dotnet/myNOC.Remootio |
.NET 8 / 10 class library | Remootio protocol implementation — also published as a NuGet package |
gateMonitor/
├── angular/ # Angular dashboard app + remootio-angular NPM library
│ ├── src/ # App UI (home component, routing)
│ └── projects/remootio-angular/ # Angular library (Remootio WebSocket client)
├── dotnet/
│ ├── GateMonitor.AppHost/ # .NET Aspire orchestrator (dev launcher)
│ ├── GateMonitor.Blazor/ # Blazor WASM dashboard
│ ├── myNOC.Remootio/ # Remootio library → NuGet: myNOC.Remootio
│ ├── tests/
│ │ ├── myNOC.Tests.Remootio/ # MSTest unit tests for the library
│ │ └── GateMonitor.Tests.Blazor/# bUnit component tests for the Blazor app
│ └── GateMonitor.slnx # .NET solution (SLNX format)
└── .github/
├── workflows/
│ ├── pr-build-test.yml # PR: build + test (.NET & Angular)
│ └── dotnet-release.yml # Main: NuGet + NPM publish + tag
└── instructions/ # Copilot architecture guidance
The Angular app connects directly from the browser to the Remootio device WebSocket (ws://{deviceIp}:8080/), authenticates, and subscribes to gate state events.
cd angular
npm ci
npm run buildService
npm start -- --openEdit angular/src/app/pages/home/home.component.ts and replace the placeholders:
this.remootioService.connect({
deviceIp: '{remootioDeviceIp}',
apiSecretKey: '{apiSecretKey}', // 64-char hex — from Remootio app
apiAuthKey: '{apiAuthKey}', // 64-char hex — from Remootio app
autoReconnect: true
});The gate image URL placeholder is also in that file. Keys are found in the Remootio mobile app under Settings → WebSocket API.
The Blazor app is a standalone WebAssembly SPA that also connects directly from the browser to the Remootio device. It uses the myNOC.Remootio library.
dotnet tool install --global Aspire.Cli --version 13.6.0
aspire run --apphost dotnet/GateMonitor.AppHost/GateMonitor.AppHost.csprojThe AppHost uses the Aspire CLI bundle. This launches the Aspire dashboard and both frontend dev servers; Angular builds its library in the npm prestart hook. Run npm ci in angular/ first. Open the Aspire dashboard URL to navigate to the app.
cd dotnet
dotnet run --project GateMonitor.Blazor
# opens http://localhost:5122Edit dotnet/GateMonitor.Blazor/wwwroot/appsettings.json:
{
"Remootio": {
"DeviceIp": "192.168.1.50",
"ApiSecretKey": "<64-char hex>",
"ApiAuthKey": "<64-char hex>",
"AutoReconnect": true,
"GateImageUrl": "http://192.168.1.51/snapshot.jpg"
}
}Note:
wwwroot/appsettings.jsonis served as a public static file. Do not commit real credentials — useappsettings.Development.json(git-ignored) or environment-local overrides.
A .NET port of the remootio-api-client-node library. Handles AES-CBC + HMAC-SHA256 encryption, the authentication challenge/response flow, and real-time state change events.
See dotnet/myNOC.Remootio/README.md for full API documentation.
Install:
dotnet add package myNOC.Remootio| Workflow | Trigger | What it does |
|---|---|---|
pr-build-test.yml |
PR → main, manual, reusable | GitVersion, .NET and Angular builds/tests, package validation |
dotnet-release.yml |
Push → main | Reuses validation, publishes through OIDC, creates git tag + GitHub release |
Publishing authentication: GitHub OIDC trusted publishing; no long-lived npm or NuGet publishing secrets are required. See Publishing setup.
GitVersion 6.8.2 uses semVer for both package registries. main uses continuous deployment mode and an empty label, so its releases have stable major.minor.patch versions without a prerelease suffix. The release workflow rejects any other version before either registry can publish, publishes npm with the latest tag, and creates a stable GitHub release. next-version: 1.0.0 intentionally makes the next stable release 1.0.0; work/ branches produce alpha versions for validation only. Keep the full git history and release tags available to CI; successful release tags provide the baseline for subsequent version increments. Before npm publishing, CI stamps the library package and lockfile with that version, builds it, and verifies that the publish directory has the exact same version. A mismatch blocks publishing. These version changes stay in the runner checkout; CI does not create a version-bump commit. NuGet uses portable PDBs in .snupkg files, publishes the GitHub repository URL/commit, and verifies SourceLink URLs and checksums for both frameworks before publishing.
Dependency versions are pinned for NuGet and captured in npm lockfiles. Angular 22.2 requires TypeScript 6.0, so TypeScript 7 is intentionally excluded. Tests use Vitest 5 with Playwright, replacing deprecated Karma/Jasmine tooling. The Angular workspace is installed once with npm ci in angular/; the library does not need a separate install.
Once the connection to Remootio is made and authenticated the web site will display a bar indicating if the gate is open or closed and 2 buttons. One to Open and one to Close the gate.
<mat-card *ngIf="isAuthenticated">
<mat-card-title>Status</mat-card-title>
<mat-card-content class="statusCards">
<div class="gateStatus"
[ngClass]="{'opened': (gateState$ | async)?.isOpen, 'closed': !(gateState$ | async)?.isOpen}">
{{ (gateState$ | async)?.description }}</div>
<button (click)='closeGate()' mat-fab class="closed" [disabled]="!(gateState$ | async)?.isOpen">Close</button>
<button (click)='openGate()' mat-fab class="opened" [disabled]="(gateState$ | async)?.isOpen">Open</button>
</mat-card-content>
</mat-card>The style sheet can be found in styles.scss. The bar is Green when the gate is closed or Red when open.
To display your own gate image, you will need to change the gateImage URL. To get the proper URL for a snap image from your camera you will need to look at your camera documentation. I use a UniFi G4 Instance, so once enabled the URL is just https://{cameraIp}/snap.jpeg. Your camera will most likely be different.
private gateImage: string = "{cameraSnapUrl}";This URL will get called every 1 second so it is updated in the UI. The Date.now() is added so the image isn't cached by the browser.
setInterval(() => this.gateImage$.next(`${this.gateImage}?${Date.now()}`), 1000);HTML to display the gate image.
<img src="{{ gateImage$ | async }}" alt="Gate" class="gateImage" />Use Node.js 26 and the workspace-local Angular CLI installed by npm ci; a global CLI is not required. Follow the Angular dashboard README for development, production builds, tests and package-version validation.