From dbb897102e7bd55566cbdc8ad48fb54400367f1d Mon Sep 17 00:00:00 2001 From: Zefek Date: Tue, 19 May 2026 21:23:38 +0200 Subject: [PATCH 1/2] =?UTF-8?q?Napojen=C3=AD=20na=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API.md | 182 ++++++++++++++++++ OdectyMVC/Application/GaugeService.cs | 28 ++- OdectyMVC/Application/IGaugeService.cs | 7 +- OdectyMVC/Business/Gauge.cs | 20 -- OdectyMVC/Contracts/IGaugeContext.cs | 4 +- .../Contracts/IGaugeListModelRepository.cs | 9 +- OdectyMVC/Contracts/IGaugeRepository.cs | 9 - OdectyMVC/Controllers/GaugeController.cs | 8 +- OdectyMVC/DataLayer/GaugeContext.cs | 18 +- OdectyMVC/DataLayer/GaugeDbContext.cs | 25 --- .../DataLayer/GaugeListModelRepository.cs | 71 ++++++- OdectyMVC/DataLayer/GaugeRepository.cs | 20 -- .../IncomeMessageBackgroundService.cs | 58 ------ OdectyMVC/DataLayer/MessageQueue.cs | 4 +- OdectyMVC/DataLayer/QueuesToConsume.cs | 6 - .../HealthChecks/GaugeFileHealthCheck.cs | 14 -- .../HealthChecks/OdectyStatHealthCheck.cs | 29 +++ OdectyMVC/Models/GaugeListModel.cs | 8 +- OdectyMVC/OdectyMVC.csproj | 1 - OdectyMVC/Options/OdectyStatSettings.cs | 6 + OdectyMVC/Program.cs | 19 +- 21 files changed, 336 insertions(+), 210 deletions(-) create mode 100644 API.md delete mode 100644 OdectyMVC/Business/Gauge.cs delete mode 100644 OdectyMVC/Contracts/IGaugeRepository.cs delete mode 100644 OdectyMVC/DataLayer/GaugeDbContext.cs delete mode 100644 OdectyMVC/DataLayer/GaugeRepository.cs delete mode 100644 OdectyMVC/DataLayer/IncomeMessageBackgroundService.cs delete mode 100644 OdectyMVC/DataLayer/QueuesToConsume.cs delete mode 100644 OdectyMVC/HealthChecks/GaugeFileHealthCheck.cs create mode 100644 OdectyMVC/HealthChecks/OdectyStatHealthCheck.cs create mode 100644 OdectyMVC/Options/OdectyStatSettings.cs 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..6afc538 100644 --- a/OdectyMVC/DataLayer/GaugeListModelRepository.cs +++ b/OdectyMVC/DataLayer/GaugeListModelRepository.cs @@ -1,20 +1,77 @@ -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) + { + FileDownloadName = fileName, + }; } - 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) From 4d23ef160420b9ae422831dbe6875c7d5494b024 Mon Sep 17 00:00:00 2001 From: Zefek Date: Tue, 19 May 2026 22:00:07 +0200 Subject: [PATCH 2/2] FileStreamResult --- OdectyMVC/DataLayer/GaugeListModelRepository.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/OdectyMVC/DataLayer/GaugeListModelRepository.cs b/OdectyMVC/DataLayer/GaugeListModelRepository.cs index 6afc538..95b6061 100644 --- a/OdectyMVC/DataLayer/GaugeListModelRepository.cs +++ b/OdectyMVC/DataLayer/GaugeListModelRepository.cs @@ -58,10 +58,7 @@ public async Task GetLastPhoto(int id, CancellationToken cancella ?? (ContentTypeProvider.TryGetContentType(fileName, out var ct) ? ct : "application/octet-stream"); var stream = await response.Content.ReadAsStreamAsync(cancellationToken); - return new FileStreamResult(stream, contentType) - { - FileDownloadName = fileName, - }; + return new FileStreamResult(stream, contentType); } private sealed class OdectyStatGaugeDto