Skip to content
malindu-ascPublic

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

193 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SmartRail

SmartRail is a public railway passenger and operations platform. It shows passengers what a train is doing right now, lets drivers report progress, and gives controllers the tools to change the service. Every time on every screen says how fresh it is, so people are never misled by old data.

Features

For What it provides
Passengers Live departures and a journey timeline, each time labelled Live, Stale or Unknown; follow a journey to get updates
Drivers Duty view, record departed and arrived at each stop, works offline and syncs when back online
Network controllers Live network board; cancel, terminate or change platform with a reason, all audited
System administrators Manage users, roles and station devices, kept separate from train operations
Stations Departure displays and an interactive touch terminal

Design principles:

  • Honest information: data freshness is always visible. Stale after 60 seconds without data, unknown after 5 minutes.
  • Scheduled time never changes when a delay is recorded.
  • Every operator change is audited (who, what, why, when).
  • Separation of duties between operating trains and administering users.
  • Information only: nothing in the system is safety-critical.

Architecture

 Flutter apps            React apps              Simulator
 passenger, driver       console, kiosk          (test train events)
        \                    |                      /
         \                   |                     /
          ------->   .NET 10 API (one deployable)  <-------
                     5 modules - one PostgreSQL DB
                            |                |
                         Keycloak       Catering (Node.js, HTTP only)
  • Modular monolith with Clean Architecture inside each module and one database schema per module.
  • CQRS with MediatR; Result and typed Error values instead of exceptions; FluentValidation in the pipeline.
  • In-process events through a transactional outbox, so a change and its events are saved together. No message broker.
  • Keycloak for identity: users, roles and tokens. The API validates tokens.
  • OpenTelemetry in every service, with Grafana, Tempo, Loki and Prometheus.

Modules

Module Responsibility
Identity Role-to-permission mapping, audit log, device tokens
Network Stations, routes, timetables, journeys, driver and unit assignments
Journeys Live journey state, confidence, stop events, departure board
Operations Cancel, terminate, platform change, disruptions, driver messages
Notifications Realtime push to clients

Each module is split into Domain, Application, Infrastructure, Api and Contracts. Modules talk to each other only through Contracts, and architecture tests enforce it.

Repository structure

SmartRail/
├── src/
│   ├── Host/
│   │   ├── SmartRail.AppHost            Aspire orchestration (dev)
│   │   ├── SmartRail.ServiceDefaults    OpenTelemetry, health checks
│   │   └── SmartRail.Api                API composition root
│   ├── BuildingBlocks/                  SharedKernel, Application.Core, Api.Core, Infrastructure.Core
│   └── Modules/
│       └── <Module>/                    Domain, Application, Infrastructure, Api, Contracts
├── apps/
│   ├── mobile/                          Flutter workspace
│   │   ├── apps/                        passenger, driver
│   │   └── packages/                    core, design_system, api_client
│   └── web/                             React workspace
│       ├── console/                     controller and admin
│       └── kiosk/                       station display and terminal
├── services/catering/                   External catering service (Node.js)
├── tools/Simulator/                     Train event simulator
├── tests/                               Architecture and module tests
├── infra/
│   ├── keycloak/                        Realm import (roles, clients, dev users)
│   └── observability/                   Collector, Tempo, Loki, Prometheus, Grafana
├── .github/workflows/                   CI, image publishing, Azure deployment
├── docker-compose.yml                   Full stack in containers
└── SmartRail.slnx

Prerequisites

Tool Version
.NET SDK 10.0
Docker Desktop recent, running
Node.js 24
Flutter 3.44 (Dart 3.12)

Running the project

Development, with Aspire

dotnet run --project src/Host/SmartRail.AppHost

Open the dashboard link printed in the terminal (usually https://localhost:17063). Aspire starts PostgreSQL, Keycloak, the API, the simulator, the catering service and both web apps, and wires their connections. Wait until every resource is green.

To include the Grafana stack:

dotnet run --project src/Host/SmartRail.AppHost -- --Observability:Enabled=true

Grafana is then at http://localhost:3002 (admin / admin), dashboard SmartRail platform.

Full stack, with Docker Compose

cp .env.example .env        # set the passwords
docker compose up -d --build
docker compose ps
URL Service
http://localhost:5080 API (/scalar for API docs in Development)
http://localhost:8080 Keycloak
http://localhost:3000 Console
http://localhost:3001 Kiosk
http://localhost:4000/health Catering
http://localhost:3002 Grafana
http://localhost:9090 Prometheus

Stop with docker compose down (add -v to remove data volumes). Do not run Aspire and Compose together, because they use the same ports.

Individual apps

# web (console or kiosk)
cd apps/web && npm install && npm run dev --workspace=console

# catering service
cd services/catering && npm install && npm run dev

# mobile
cd apps/mobile && flutter pub get
cd apps/passenger && flutter run      # or apps/driver

Testing

dotnet test                                                  # unit, architecture and API tests (API tests need Docker)
npm run build --workspace=console --prefix apps/web          # web build
npm run typecheck --prefix services/catering                 # catering typecheck
cd apps/mobile/apps/passenger && flutter test                # mobile tests

CI runs all of the above plus a Docker build of every image on each push and pull request.

Signing in (development)

Keycloak seeds one user per role: passenger, driver, controller, scheduler and admin. Their development passwords are in infra/keycloak/smartrail-realm.json. They are for local use only.

Getting a token (development)

scripts/get-token.sh controller
curl -H "Authorization: Bearer $(scripts/get-token.sh controller)" http://localhost:5080/api/identity/whoami

Every endpoint requires a signed-in user unless it is explicitly marked anonymous; a test fails the build if an unexpected endpoint becomes public. Roles map to the Driver, Controller, Scheduler and Admin policies.

Network data (public API)

Stations and routes are public, read-only and need no token. Names follow ?lang= or Accept-Language (en default, si).

curl "http://localhost:5080/api/network/stations?q=oak&lang=si"
curl  http://localhost:5080/api/network/stations/CEN
curl  http://localhost:5080/api/network/routes
curl  http://localhost:5080/api/network/routes/HILL

Scheduled timetable (public, read-only). date is yyyy-MM-dd and defaults to today in the network time zone (Network__TimeZone, default Asia/Colombo). Times after midnight continue past 24 (24:24).

curl "http://localhost:5080/api/network/stations/CEN/timetable?date=2026-10-14"
curl "http://localhost:5080/api/network/stations/CEN/timetable?date=2026-10-14&kind=arrivals"
curl "http://localhost:5080/api/network/services/<serviceId>?date=2026-10-14"

Journeys

Journeys are generated from the timetable for the next 7 days at 01:00 network time, and once at startup when Journeys__Generation__RunOnStartup=true. Journeys__Seed__DemoFleet=true adds 8 demo trainsets, a roster and assignments so every generated journey is Ready (Aspire and Docker Compose set both). Reads are public and cached for 5 seconds.

curl "http://localhost:5080/api/journeys/boards/CEN?count=10"
curl  http://localhost:5080/api/journeys/<journeyId>
# manual journey: needs a token with the scheduler role (scripts/get-token.sh)
curl -X POST http://localhost:5080/api/journeys/manual -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json"   -d '{"routeCode":"MAIN","trainNumber":"SP1","serviceDate":"2026-10-14","reason":"Festival special","stops":[{"stationCode":"CEN","departure":"22:00"},{"stationCode":"OAK","arrival":"22:20"}]}'

Live feed ingest

The train detection feed (and the simulator) send live data with their own source key, not a user token. Send X-Source-Code and X-Source-Key headers; a wrong or missing key answers 401. Sequences are one increasing counter per source, across both endpoints. Each message in a batch (1 to 100) gets its own result.

curl -X POST http://localhost:5080/api/ingest/stop-events -H "X-Source-Code: SIM" -H "X-Source-Key: $FEED_KEY" -H "Content-Type: application/json"   -d '{"events":[{"sequence":1,"journeyId":"<journeyId>","stopSequence":1,"type":"departed","occurredAt":"2026-10-14T02:08:00Z"}]}'
curl -X POST http://localhost:5080/api/ingest/positions -H "X-Source-Code: SIM" -H "X-Source-Key: $FEED_KEY" -H "Content-Type: application/json"   -d '{"readings":[{"sequence":2,"vehicleCode":"MAIN-F","recordedAt":"2026-10-14T02:10:00Z","fromStation":"CEN","toStation":"OAK","progress":0.4,"speedKmh":72}]}'

type is arrived, departed or passed. Aspire and Docker Compose create two development sources (SIM rank 1 and SIM-B rank 2) from Identity__FeedSources__Seed__*; their keys are for development only. Live thresholds are in Journeys:Live (stale after 60 s, Unknown after 300 s).

The demo network is inserted at startup when Network__Seed__ReferenceData=true (set by the Aspire AppHost and Docker Compose). It only adds missing rows and never changes existing ones.

Database migrations

Each module has its own DbContext and schema. From the repository root:

dotnet ef migrations add <Name> \
  --project src/Modules/<Module>/SmartRail.Modules.<Module>.Infrastructure \
  --context <Module>DbContext \
  --output-dir Persistence/Migrations

Migrations are applied automatically on startup in Development only.

Observability

Services export OpenTelemetry to a collector, which sends traces to Tempo, logs to Loki and metrics to Prometheus. Grafana is pre-provisioned with these data sources, a SmartRail platform dashboard and alert rules for API errors, latency, outbox failures and missing telemetry.

CI/CD

Workflow Purpose
ci.yml Backend build and tests, web build, catering typecheck, Flutter analyze and test, Docker image builds
cd.yml Publishes images to GitHub Container Registry from main
deploy-azure.yml Manual azd deployment to Azure Container Apps

Security checks

Check Where
Secret scanning (Gitleaks) security.yml
Static analysis (CodeQL, C# and TypeScript) security.yml
Vulnerable NuGet and npm packages security.yml
Repository and Dockerfile scan (Trivy) security.yml
Image scan (Trivy) ci.yml, per image
Dependency updates dependabot.yml, weekly

Troubleshooting

Symptom Fix
dotnet run prints "Building..." and returns Reset Aspire's socket folder: Remove-Item -Recurse -Force "$env:USERPROFILE\.aspire\cli\bch", then run again. Logs are in %USERPROFILE%\.aspire\logs
Build says a file is locked by SmartRail.AppHost The AppHost is still running. Press Ctrl+C in its terminal
Cannot connect to the Docker daemon Start Docker Desktop and wait until it is running
Port already in use Stop the other stack (Aspire or Compose)
Keycloak shows Unhealthy It is usually still starting. Wait a minute, then check its log
flutter pub get fails on the workspace Run it from apps/mobile, not from a package folder
Grafana panels are empty Send a few requests to the API; panels fill once there is traffic

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages