diff --git a/API.md b/API.md
new file mode 100644
index 0000000..17bf42b
--- /dev/null
+++ b/API.md
@@ -0,0 +1,182 @@
+# OdectyStat – API reference
+
+Read-only HTTP API mikroslužby pro odečty měřidel. Určeno pro konzumaci gateway API,
+které sedí před touto službou a řeší autorizaci.
+
+- **Base URL**: `http://127.0.0.1:5080` (konfigurovatelné v `appSettings.json` přes `Kestrel:Endpoints:Http:Url`)
+- **Bind**: pouze loopback – nedostupné mimo server
+- **Autentizace**: žádná (autorizaci dělá gateway)
+- **Write path**: tato služba zápisové endpointy **nevystavuje**, zápisy chodí výhradně přes RabbitMQ
+
+## Konvence
+
+- Všechny odpovědi: `application/json; charset=utf-8`, kromě `lastphoto` (binární)
+- Datum/čas: ISO 8601 bez timezone, **lokální čas serveru**
+- Chyby: RFC 7807 `application/problem+json`
+
+---
+
+## `GET /api/gauges`
+
+Seznam všech měřidel s posledním stavem.
+
+### Response `200 OK`
+
+```json
+[
+ {
+ "id": 1,
+ "description": "Vodoměr studna",
+ "type": "Water",
+ "lastValue": 123.4567,
+ "lastMeasurementAt": "2026-05-18T08:15:00",
+ "hasPhoto": true
+ },
+ {
+ "id": 2,
+ "description": "Elektroměr HDO",
+ "type": "Electricity",
+ "lastValue": 45678.0,
+ "lastMeasurementAt": null,
+ "hasPhoto": false
+ }
+]
+```
+
+| Pole | Typ | Popis |
+|---|---|---|
+| `id` | `int` | ID měřidla |
+| `description` | `string` | Popis (`Gauge.Description`) |
+| `type` | `string` | Typ měřidla (`Gauge.Type`, např. `"Water"`, `"Electricity"`) |
+| `lastValue` | `decimal` | Poslední kumulativní stav |
+| `lastMeasurementAt` | `DateTime?` | Čas posledního měření; `null` pokud měřidlo nikdy nezaznamenalo měření |
+| `hasPhoto` | `bool` | `true` ⇒ volání `/lastphoto` má smysl; `false` ⇒ vrátí `404` |
+
+### Statusy
+
+- `200` – vždy (i prázdné pole, pokud žádná měřidla nejsou)
+
+---
+
+## `GET /api/gauges/{id}/lastphoto`
+
+Binární obsah fotky z posledního měření daného měřidla.
+
+### Path parametr
+
+| Parametr | Typ | Popis |
+|---|---|---|
+| `id` | `int` | ID měřidla |
+
+### Response `200 OK`
+
+- `Content-Type`: `image/jpeg` / `image/png` / `image/*` podle přípony (detekováno `FileExtensionContentTypeProvider`); fallback `application/octet-stream`
+- `Content-Disposition`: `attachment; filename="
"`
+- Tělo: surový obsah souboru
+
+### Statusy
+
+- `200` – fotka existuje a vrátila se
+- `404` – buď neexistuje žádný `GaugeMeasurement` s vyplněným `ImagePath` pro toto měřidlo,
+ nebo se soubor nepodařilo dohledat na disku (loguje se warning)
+- `400` – `id` není validní int (handluje routing)
+
+### Poznámky
+
+Gateway by měla nejdřív zavolat `/api/gauges` a teprve když je `hasPhoto: true`,
+volat `/lastphoto`. Šetří to round-trip přes 404.
+
+Soubor se hledá pod cestou `RecognizedSuccessFolder/{id}/{yyyy-MM-dd}/{ImagePath}`,
+kde datum se odvozuje z `MeasurementDateTime` ±1 den (kvůli rozdílu mezi
+`MeasurementDateTime` v DB a creation-time původního souboru použitým při ukládání).
+
+---
+
+## `GET /health/live`
+
+Liveness probe. Vrátí `200` vždy, dokud proces běží. Neprovádí žádné kontroly závislostí.
+
+```json
+{
+ "status": "Healthy",
+ "totalDuration": "00:00:00.0010973",
+ "entries": {}
+}
+```
+
+### Statusy
+
+- `200` – `status: "Healthy"`
+- `503` – `status: "Unhealthy"` (v praxi nikdy, dokud proces žije)
+
+---
+
+## `GET /health/ready`
+
+Readiness probe. Zkontroluje, že jsou dostupné všechny závislosti potřebné pro obsluhu requestů.
+
+### Kontrolované závislosti
+
+| Entry | Co se kontroluje |
+|---|---|
+| `sqlserver-odecty` | SQL Server (connection string `Odecty`) |
+| `postgres-homeassistant` | Postgres (connection string `HomeAssistant`) |
+| `postgres-diagnostics` | Postgres (connection string `Diagnostics`) |
+| `rabbitmq` | AMQP connection na RabbitMQ (z `OdectySettings`) |
+
+### Response `200 OK`
+
+```json
+{
+ "status": "Healthy",
+ "totalDuration": "00:00:00.0096718",
+ "entries": {
+ "sqlserver-odecty": {
+ "data": {},
+ "description": null,
+ "duration": "00:00:00.0028340",
+ "status": "Healthy",
+ "tags": ["ready"]
+ },
+ "rabbitmq": {
+ "data": {},
+ "description": null,
+ "duration": "00:00:00.0021110",
+ "status": "Healthy",
+ "tags": ["ready"]
+ }
+ }
+}
+```
+
+### Statusy
+
+- `200` – všechny závislosti `Healthy`
+- `503` – aspoň jedna `Unhealthy` nebo `Degraded`; v `entries` je vidět která
+
+---
+
+## Chybové odpovědi (RFC 7807)
+
+Neočekávané chyby vrací `application/problem+json`:
+
+```json
+{
+ "type": "https://tools.ietf.org/html/rfc7234#section-5.5.1",
+ "title": "An error occurred while processing your request.",
+ "status": 500,
+ "traceId": "00-abc...-01"
+}
+```
+
+`traceId` koresponduje s OpenTelemetry trace ID, kterým si gateway může dohledat
+distribuovaný trace v collectoru.
+
+---
+
+## Observability
+
+- **OTLP gRPC exporter** (logs, traces, metrics) → cílí na collector běžící na localhost
+- **Service name**: `OdectyStat`
+- **Instrumentace**: ASP.NET Core, HttpClient, EF Core, .NET Runtime metriky
+- Endpoint kolektoru řízený env var `OTEL_EXPORTER_OTLP_ENDPOINT` (default `http://localhost:4317`)
diff --git a/OdectyMVC/Application/GaugeService.cs b/OdectyMVC/Application/GaugeService.cs
index 70ac59b..b402449 100644
--- a/OdectyMVC/Application/GaugeService.cs
+++ b/OdectyMVC/Application/GaugeService.cs
@@ -1,4 +1,5 @@
-using Microsoft.Extensions.Options;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
using OdectyMVC.Contracts;
using OdectyMVC.Dto;
using OdectyMVC.Options;
@@ -18,16 +19,13 @@ public GaugeService(IGaugeContext context, IOptions options)
public async Task AddNewValue(int gaugeId, decimal value, CancellationToken cancellationToken)
{
- var gauge = await context.GaugeRepository.GetGauge(gaugeId, cancellationToken);
- gauge.SetNewValue(value);
- await context.SaveChangesAsync(cancellationToken);
await context.MessageQueue.Publish(MessageQueueRoutingKeys.GaugeMVC_Gauge_Statechanged,
- new
- {
- GaugeId = gaugeId,
- Value = value,
- Datetime = DateTime.Now
- }, cancellationToken);
+ new
+ {
+ GaugeId = gaugeId,
+ Value = value,
+ Datetime = DateTime.Now
+ }, cancellationToken);
}
public async Task> GetGaugeList(CancellationToken cancellationToken)
@@ -35,21 +33,19 @@ await context.MessageQueue.Publish(MessageQueueRoutingKeys.GaugeMVC_Gauge_Statec
return await context.GaugeListModelRepository.GetGaugeList(cancellationToken);
}
- public async Task UpdateGaugeState(int gaugeId, decimal value, CancellationToken cancellationToken)
+ public async Task GetLastPhoto(int gaugeId, CancellationToken cancellationToken)
{
- var gauge = await context.GaugeRepository.GetGauge(gaugeId, cancellationToken);
- gauge.LastValue = value;
- await context.SaveChangesAsync(cancellationToken);
+ return await context.GaugeListModelRepository.GetLastPhoto(gaugeId, cancellationToken);
}
public async Task SaveFileForGauge(int gaugeId, MemoryStream memoryStream, CancellationToken cancellationToken)
{
- var gauge = await context.GaugeRepository.GetGauge(gaugeId, cancellationToken);
+ var gauge = await context.GaugeListModelRepository.GetById(gaugeId, cancellationToken);
if (gauge == null)
{
throw new ArgumentException($"Gauge with id {gaugeId} not found");
}
- var fileName = $"{gauge.Name}_{Guid.NewGuid():N}.jpg";
+ var fileName = $"{gauge.Type}_{Guid.NewGuid():N}.jpg";
memoryStream.Position = 0;
await using var stream = File.Create(string.Format(options.Value.Path, gaugeId, fileName));
await memoryStream.CopyToAsync(stream, cancellationToken);
diff --git a/OdectyMVC/Application/IGaugeService.cs b/OdectyMVC/Application/IGaugeService.cs
index 35ae5b6..0ccea07 100644
--- a/OdectyMVC/Application/IGaugeService.cs
+++ b/OdectyMVC/Application/IGaugeService.cs
@@ -1,4 +1,5 @@
-using OdectyMVC.Models;
+using Microsoft.AspNetCore.Mvc;
+using OdectyMVC.Models;
namespace OdectyMVC.Application
{
@@ -6,7 +7,7 @@ public interface IGaugeService
{
Task AddNewValue(int gaugeId, decimal value, CancellationToken cancellationToken);
Task> GetGaugeList(CancellationToken cancellationToken);
- Task UpdateGaugeState(int gaugeId, decimal value, CancellationToken cancellationToken);
Task SaveFileForGauge(int gaugeId, MemoryStream memoryStream, CancellationToken cancellationToken);
+ Task GetLastPhoto(int gaugeId, CancellationToken cancellationToken);
}
-}
\ No newline at end of file
+}
diff --git a/OdectyMVC/Business/Gauge.cs b/OdectyMVC/Business/Gauge.cs
deleted file mode 100644
index fe6cdea..0000000
--- a/OdectyMVC/Business/Gauge.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
-
-namespace OdectyMVC.Business
-{
- [Table("Gauge")]
- public class Gauge
- {
- [Key]
- public int Id { get; set; }
- public string Description { get; set; }
- public decimal LastValue { get; set; }
- public string Name { get; set; }
-
- public void SetNewValue(decimal newValue)
- {
- LastValue = newValue;
- }
- }
-}
diff --git a/OdectyMVC/Contracts/IGaugeContext.cs b/OdectyMVC/Contracts/IGaugeContext.cs
index 6a2425d..cc2bb26 100644
--- a/OdectyMVC/Contracts/IGaugeContext.cs
+++ b/OdectyMVC/Contracts/IGaugeContext.cs
@@ -1,10 +1,8 @@
-namespace OdectyMVC.Contracts
+namespace OdectyMVC.Contracts
{
public interface IGaugeContext
{
- IGaugeRepository GaugeRepository { get; }
IGaugeListModelRepository GaugeListModelRepository { get; }
IMessageQueue MessageQueue { get; }
- Task SaveChangesAsync(CancellationToken cancellationToken);
}
}
diff --git a/OdectyMVC/Contracts/IGaugeListModelRepository.cs b/OdectyMVC/Contracts/IGaugeListModelRepository.cs
index 7ba8202..5b440b7 100644
--- a/OdectyMVC/Contracts/IGaugeListModelRepository.cs
+++ b/OdectyMVC/Contracts/IGaugeListModelRepository.cs
@@ -1,7 +1,12 @@
-namespace OdectyMVC.Contracts
+using Microsoft.AspNetCore.Mvc;
+using OdectyMVC.Models;
+
+namespace OdectyMVC.Contracts
{
public interface IGaugeListModelRepository
{
- Task> GetGaugeList(CancellationToken cancellationToken);
+ Task> GetGaugeList(CancellationToken cancellationToken);
+ Task GetById(int id, CancellationToken cancellationToken);
+ Task GetLastPhoto(int id, CancellationToken cancellationToken);
}
}
diff --git a/OdectyMVC/Contracts/IGaugeRepository.cs b/OdectyMVC/Contracts/IGaugeRepository.cs
deleted file mode 100644
index b4bfdd3..0000000
--- a/OdectyMVC/Contracts/IGaugeRepository.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using OdectyMVC.Business;
-
-namespace OdectyMVC.Contracts
-{
- public interface IGaugeRepository
- {
- Task GetGauge(int id, CancellationToken cancellationToken);
- }
-}
diff --git a/OdectyMVC/Controllers/GaugeController.cs b/OdectyMVC/Controllers/GaugeController.cs
index 69ae40c..c3b91ef 100644
--- a/OdectyMVC/Controllers/GaugeController.cs
+++ b/OdectyMVC/Controllers/GaugeController.cs
@@ -1,4 +1,4 @@
-using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OdectyMVC.Application;
@@ -26,4 +26,10 @@ public async Task GaugeByImage(int id, CancellationToken cancella
await gaugeService.SaveFileForGauge(id, memoryStream, cancellationToken);
return Ok();
}
+
+ [HttpGet("{id}/lastphoto")]
+ public Task GetLastPhoto(int id, CancellationToken cancellationToken)
+ {
+ return gaugeService.GetLastPhoto(id, cancellationToken);
+ }
}
diff --git a/OdectyMVC/DataLayer/GaugeContext.cs b/OdectyMVC/DataLayer/GaugeContext.cs
index b699afd..a832797 100644
--- a/OdectyMVC/DataLayer/GaugeContext.cs
+++ b/OdectyMVC/DataLayer/GaugeContext.cs
@@ -1,30 +1,18 @@
-using OdectyMVC.Contracts;
+using OdectyMVC.Contracts;
namespace OdectyMVC.DataLayer
{
public class GaugeContext : IGaugeContext
{
- private readonly GaugeDbContext context;
-
- public GaugeContext(IGaugeRepository gaugeRepository,
+ public GaugeContext(
IGaugeListModelRepository gaugeListModelRepository,
- IMessageQueue messageQueue,
- GaugeDbContext context)
+ IMessageQueue messageQueue)
{
- GaugeRepository = gaugeRepository;
GaugeListModelRepository = gaugeListModelRepository;
MessageQueue = messageQueue;
- this.context = context;
}
- public IGaugeRepository GaugeRepository { get; }
public IGaugeListModelRepository GaugeListModelRepository { get; }
public IMessageQueue MessageQueue { get; }
-
- public Task SaveChangesAsync(CancellationToken cancellationToken)
- {
- context.SaveChanges();
- return Task.CompletedTask;
- }
}
}
diff --git a/OdectyMVC/DataLayer/GaugeDbContext.cs b/OdectyMVC/DataLayer/GaugeDbContext.cs
deleted file mode 100644
index 0140773..0000000
--- a/OdectyMVC/DataLayer/GaugeDbContext.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using Newtonsoft.Json;
-using OdectyMVC.Business;
-using OdectyMVC.Models;
-
-namespace OdectyMVC.DataLayer
-{
- public class GaugeDbContext
- {
- public GaugeDbContext()
- {
- var gauges = File.ReadAllText("GaugeList.json");
- Gauges = JsonConvert.DeserializeObject>(gauges);
- GaugeModels = JsonConvert.DeserializeObject>(gauges);
- }
- public List Gauges { get; set; }
-
- public List GaugeModels { get; set; }
-
- public void SaveChanges()
- {
- File.WriteAllText("GaugeList.json", JsonConvert.SerializeObject(Gauges, Formatting.Indented));
-
- }
- }
-}
diff --git a/OdectyMVC/DataLayer/GaugeListModelRepository.cs b/OdectyMVC/DataLayer/GaugeListModelRepository.cs
index 8875dd3..95b6061 100644
--- a/OdectyMVC/DataLayer/GaugeListModelRepository.cs
+++ b/OdectyMVC/DataLayer/GaugeListModelRepository.cs
@@ -1,20 +1,74 @@
-using OdectyMVC.Contracts;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.StaticFiles;
+using Microsoft.Net.Http.Headers;
+using OdectyMVC.Contracts;
using OdectyMVC.Models;
+using System.Net;
+using System.Text.Json;
namespace OdectyMVC.DataLayer
{
internal class GaugeListModelRepository : IGaugeListModelRepository
{
- private GaugeDbContext gaugeContext;
+ private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
+ private static readonly FileExtensionContentTypeProvider ContentTypeProvider = new();
- public GaugeListModelRepository(GaugeDbContext gaugeContext)
+ private readonly HttpClient httpClient;
+
+ public GaugeListModelRepository(HttpClient httpClient)
+ {
+ this.httpClient = httpClient;
+ }
+
+ public async Task> GetGaugeList(CancellationToken cancellationToken)
+ {
+ await using var stream = await httpClient.GetStreamAsync("api/gauges", cancellationToken);
+ var dtos = await JsonSerializer.DeserializeAsync>(stream, JsonOptions, cancellationToken)
+ ?? new List();
+ return dtos.Select(d => new GaugeListModel
+ {
+ Id = d.Id,
+ Description = d.Description,
+ Type = d.Type,
+ LastValue = d.LastValue,
+ LastMeasurementAt = d.LastMeasurementAt,
+ HasPhoto = d.HasPhoto,
+ });
+ }
+
+ public async Task GetById(int id, CancellationToken cancellationToken)
+ {
+ var gauges = await GetGaugeList(cancellationToken);
+ return gauges.FirstOrDefault(g => g.Id == id);
+ }
+
+ public async Task GetLastPhoto(int id, CancellationToken cancellationToken)
{
- this.gaugeContext = gaugeContext;
+ var response = await httpClient.GetAsync($"api/gauges/{id}/lastphoto", HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+ if (response.StatusCode == HttpStatusCode.NotFound)
+ {
+ return new NotFoundResult();
+ }
+ response.EnsureSuccessStatusCode();
+
+ var fileName = response.Content.Headers.ContentDisposition?.FileNameStar
+ ?? response.Content.Headers.ContentDisposition?.FileName?.Trim('"')
+ ?? $"gauge_{id}";
+ var contentType = response.Content.Headers.ContentType?.MediaType
+ ?? (ContentTypeProvider.TryGetContentType(fileName, out var ct) ? ct : "application/octet-stream");
+
+ var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
+ return new FileStreamResult(stream, contentType);
}
- public Task> GetGaugeList(CancellationToken cancellationToken)
+ private sealed class OdectyStatGaugeDto
{
- return Task.FromResult(gaugeContext.GaugeModels.OrderBy(k => k.Id) as IEnumerable);
+ public int Id { get; set; }
+ public string Description { get; set; }
+ public string Type { get; set; }
+ public decimal LastValue { get; set; }
+ public DateTime? LastMeasurementAt { get; set; }
+ public bool HasPhoto { get; set; }
}
}
-}
\ No newline at end of file
+}
diff --git a/OdectyMVC/DataLayer/GaugeRepository.cs b/OdectyMVC/DataLayer/GaugeRepository.cs
deleted file mode 100644
index 7307306..0000000
--- a/OdectyMVC/DataLayer/GaugeRepository.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using OdectyMVC.Business;
-using OdectyMVC.Contracts;
-
-namespace OdectyMVC.DataLayer
-{
- internal class GaugeRepository : IGaugeRepository
- {
- private GaugeDbContext gaugeContext;
-
- public GaugeRepository(GaugeDbContext gaugeContext)
- {
- this.gaugeContext = gaugeContext;
- }
-
- public Task GetGauge(int id, CancellationToken cancellationToken)
- {
- return Task.FromResult(gaugeContext.Gauges.FirstOrDefault(k => k.Id == id));
- }
- }
-}
\ No newline at end of file
diff --git a/OdectyMVC/DataLayer/IncomeMessageBackgroundService.cs b/OdectyMVC/DataLayer/IncomeMessageBackgroundService.cs
deleted file mode 100644
index d3ff075..0000000
--- a/OdectyMVC/DataLayer/IncomeMessageBackgroundService.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-
-using Microsoft.Extensions.Options;
-using Newtonsoft.Json;
-using OdectyMVC.Application;
-using OdectyMVC.Options;
-using RabbitMQ.Client;
-using RabbitMQ.Client.Events;
-
-namespace OdectyMVC.DataLayer;
-
-public class IncomeMessageBackgroundService : BackgroundService, IDisposable
-{
- private readonly IOptions options;
- private readonly IServiceProvider serviceProvider;
- private readonly RabbitMQProvider rabbitMQProvider;
- private IChannel channel;
-
- public IncomeMessageBackgroundService(IOptions options, IServiceProvider serviceProvider, RabbitMQProvider rabbitMQProvider)
- {
- this.options = options;
- this.serviceProvider = serviceProvider;
- this.rabbitMQProvider = rabbitMQProvider;
- }
-
- protected override async Task ExecuteAsync(CancellationToken stoppingToken)
- {
- channel = await rabbitMQProvider.CreateModel();
- if (channel != null)
- {
- var consumer = new AsyncEventingBasicConsumer(channel);
- consumer.ReceivedAsync += async (model, ea) =>
- {
- var body = ea.Body.ToArray();
- var message = System.Text.Encoding.UTF8.GetString(body);
- var gaugeState = JsonConvert.DeserializeObject(message);
- using var scope = serviceProvider.CreateScope();
- var gaugeService = scope.ServiceProvider.GetRequiredService();
- await gaugeService.UpdateGaugeState((int)gaugeState.gaugeId, (decimal)gaugeState.value, stoppingToken);
- await channel.BasicAckAsync(ea.DeliveryTag, false, stoppingToken);
- };
- await channel.BasicConsumeAsync(queue: QueuesToConsume.OdectyMVC,
- autoAck: false,
- consumer: consumer, stoppingToken);
- }
- }
-
- public override void Dispose()
- {
- channel?.Dispose();
- base.Dispose();
- }
-
- public override Task StopAsync(CancellationToken cancellationToken)
- {
- channel?.Dispose();
- return base.StopAsync(cancellationToken);
- }
-}
diff --git a/OdectyMVC/DataLayer/MessageQueue.cs b/OdectyMVC/DataLayer/MessageQueue.cs
index 71a54be..9c74de8 100644
--- a/OdectyMVC/DataLayer/MessageQueue.cs
+++ b/OdectyMVC/DataLayer/MessageQueue.cs
@@ -1,9 +1,9 @@
using Microsoft.Extensions.Options;
-using Newtonsoft.Json;
using OdectyMVC.Contracts;
using OdectyMVC.Options;
using RabbitMQ.Client;
using System.Text;
+using System.Text.Json;
namespace OdectyMVC.DataLayer
{
@@ -22,7 +22,7 @@ public async Task Publish(string routingKey, object message, CancellationToken c
{
if (model != null)
{
- await model.BasicPublishAsync(options.Value.ExchangeName, routingKey, true, new ReadOnlyMemory(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message))), cancellationToken);
+ await model.BasicPublishAsync(options.Value.ExchangeName, routingKey, true, new ReadOnlyMemory(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(message))), cancellationToken);
}
}
}
diff --git a/OdectyMVC/DataLayer/QueuesToConsume.cs b/OdectyMVC/DataLayer/QueuesToConsume.cs
deleted file mode 100644
index b1f74a9..0000000
--- a/OdectyMVC/DataLayer/QueuesToConsume.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace OdectyMVC.DataLayer;
-
-public static class QueuesToConsume
-{
- public const string OdectyMVC = "OdectyMVC";
-}
diff --git a/OdectyMVC/HealthChecks/GaugeFileHealthCheck.cs b/OdectyMVC/HealthChecks/GaugeFileHealthCheck.cs
deleted file mode 100644
index 955cf8d..0000000
--- a/OdectyMVC/HealthChecks/GaugeFileHealthCheck.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using Microsoft.Extensions.Diagnostics.HealthChecks;
-
-namespace OdectyMVC.HealthChecks;
-
-public class GaugeFileHealthCheck : IHealthCheck
-{
- private const string FileName = "GaugeList.json";
-
- public Task CheckHealthAsync(
- HealthCheckContext context, CancellationToken cancellationToken = default)
- => Task.FromResult(File.Exists(FileName)
- ? HealthCheckResult.Healthy($"{FileName} is present")
- : HealthCheckResult.Unhealthy($"{FileName} is missing"));
-}
diff --git a/OdectyMVC/HealthChecks/OdectyStatHealthCheck.cs b/OdectyMVC/HealthChecks/OdectyStatHealthCheck.cs
new file mode 100644
index 0000000..eae2328
--- /dev/null
+++ b/OdectyMVC/HealthChecks/OdectyStatHealthCheck.cs
@@ -0,0 +1,29 @@
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace OdectyMVC.HealthChecks;
+
+public class OdectyStatHealthCheck : IHealthCheck
+{
+ private readonly HttpClient httpClient;
+
+ public OdectyStatHealthCheck(HttpClient httpClient)
+ {
+ this.httpClient = httpClient;
+ }
+
+ public async Task CheckHealthAsync(
+ HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ using var response = await httpClient.GetAsync("health/ready", HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+ return response.IsSuccessStatusCode
+ ? HealthCheckResult.Healthy($"OdectyStat /health/ready returned {(int)response.StatusCode}")
+ : HealthCheckResult.Unhealthy($"OdectyStat /health/ready returned {(int)response.StatusCode}");
+ }
+ catch (Exception ex)
+ {
+ return HealthCheckResult.Unhealthy("OdectyStat is unreachable", ex);
+ }
+ }
+}
diff --git a/OdectyMVC/Models/GaugeListModel.cs b/OdectyMVC/Models/GaugeListModel.cs
index 0b694f1..283135c 100644
--- a/OdectyMVC/Models/GaugeListModel.cs
+++ b/OdectyMVC/Models/GaugeListModel.cs
@@ -1,11 +1,13 @@
-namespace OdectyMVC.Models
+namespace OdectyMVC.Models
{
public class GaugeListModel
{
+ public int Id { get; set; }
public string Description { get; set; }
- public decimal LastValue { get; set; }
public string Type { get; set; }
+ public decimal LastValue { get; set; }
+ public DateTime? LastMeasurementAt { get; set; }
+ public bool HasPhoto { get; set; }
public decimal? NewValue { get; set; }
- public int Id { get; set; }
}
}
diff --git a/OdectyMVC/OdectyMVC.csproj b/OdectyMVC/OdectyMVC.csproj
index 7f47d22..db4278c 100644
--- a/OdectyMVC/OdectyMVC.csproj
+++ b/OdectyMVC/OdectyMVC.csproj
@@ -10,7 +10,6 @@
-
diff --git a/OdectyMVC/Options/OdectyStatSettings.cs b/OdectyMVC/Options/OdectyStatSettings.cs
new file mode 100644
index 0000000..a862df9
--- /dev/null
+++ b/OdectyMVC/Options/OdectyStatSettings.cs
@@ -0,0 +1,6 @@
+namespace OdectyMVC.Options;
+
+public class OdectyStatSettings
+{
+ public string BaseUrl { get; set; }
+}
diff --git a/OdectyMVC/Program.cs b/OdectyMVC/Program.cs
index e986fec..a1b27a3 100644
--- a/OdectyMVC/Program.cs
+++ b/OdectyMVC/Program.cs
@@ -55,19 +55,28 @@
builder.Services.Configure(builder.Configuration.GetSection("RabbitMQSettings"));
builder.Services.Configure(builder.Configuration.GetSection("GaugeImageLocation"));
builder.Services.Configure(builder.Configuration.GetSection("BasicAuthentication"));
+builder.Services.Configure(builder.Configuration.GetSection("OdectyStat"));
builder.Services.AddScoped();
builder.Services.AddScoped();
-builder.Services.AddScoped();
-builder.Services.AddScoped();
-builder.Services.AddScoped();
+var odectyStatBaseUrl = builder.Configuration["OdectyStat:BaseUrl"]
+ ?? throw new InvalidOperationException("Missing configuration: OdectyStat:BaseUrl");
+
+builder.Services.AddHttpClient(c =>
+{
+ c.BaseAddress = new Uri(odectyStatBaseUrl);
+});
+builder.Services.AddHttpClient(c =>
+{
+ c.BaseAddress = new Uri(odectyStatBaseUrl);
+});
+
builder.Services.AddSingleton();
builder.Services.AddSingleton();
-builder.Services.AddHostedService();
builder.Services.AddHealthChecks()
.AddCheck("rabbitmq", tags: new[] { "ready" })
- .AddCheck("gauge-file", tags: new[] { "ready" });
+ .AddCheck("odecty-stat", tags: new[] { "ready" });
#if !DEBUG
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)