Docker Compose bot + Google Api
Build App / Build (push) Failing after 3s

This commit is contained in:
2026-08-06 16:23:50 +02:00
parent c9a3c252e1
commit 937b8554dd
44 changed files with 3360 additions and 74 deletions
@@ -0,0 +1,101 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using LaDOSE.Business.Interface;
using LaDOSE.DTO;
using LaDOSE.Entity;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace LaDOSE.Api.Controllers
{
/// <summary>
/// Pushes ranking tables into the club's Google Spreadsheet.
///
/// The tables arrive already built: the browser reuses the same buildRanking() that produces
/// the CSV export, so the sheet and the CSV cannot drift. This controller resolves the target
/// from configuration, validates, and relays — see SheetsExportService.
///
/// Returns IActionResult with explicit ProducesResponseType, like UsersController: the API has
/// no exception middleware, so an escaping throw reaches the browser as an HTML developer page
/// that the client can only report as a bare status.
/// </summary>
[Authorize]
[Produces("application/json")]
[Route("api/[controller]")]
public class SheetsController : ControllerBase
{
private readonly ISheetsExportService _service;
private readonly IMapper _mapper;
private readonly ILogger<SheetsController> _logger;
public SheetsController(IMapper mapper, ISheetsExportService service, ILogger<SheetsController> logger)
{
_mapper = mapper;
_service = service;
_logger = logger;
}
/// <summary>
/// Whether the export is usable and where it points, so the panel can render itself and
/// disable the button when the server is not set up. Carries no secret and no key path.
/// </summary>
[HttpGet("Config")]
[ProducesResponseType(typeof(SheetsConfigDTO), StatusCodes.Status200OK)]
public IActionResult GetConfig()
{
return Ok(_mapper.Map<SheetsConfigDTO>(_service.GetConfig()));
}
/// <summary>
/// Writes one tab per table, in the order given — oldest event first, each tab holding the
/// cumulative result up to and including its own event.
///
/// Existing tabs of the same name are cleared and rewritten in place, keeping their
/// formatting; anything typed into them by hand is lost. Tabs not named in the request are
/// never touched or deleted.
/// </summary>
[HttpPost("Export")]
[ProducesResponseType(typeof(SheetExportResultDTO), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status502BadGateway)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IActionResult> Export([FromBody] SheetExportRequestDTO dto, CancellationToken ct)
{
if (dto?.Tabs == null || dto.Tabs.Count == 0)
return BadRequest(new { message = "No table to write." });
var request = _mapper.Map<SheetExportRequest>(dto);
try
{
var result = await _service.ExportAsync(request, ct);
_logger.LogInformation(
"Sheets export by user {User}: {TabCount} tabs to spreadsheet {SpreadsheetId} via {Writer}",
User?.Identity?.Name, result.Tabs?.Count ?? 0, result.SpreadsheetId, result.Writer);
return Ok(_mapper.Map<SheetExportResultDTO>(result));
}
catch (SheetsExportException ex)
{
_logger.LogWarning(ex, "Sheets export refused ({StatusCode})", ex.StatusCode);
return StatusCode(ex.StatusCode, new { message = ex.Message });
}
catch (OperationCanceledException)
{
// The caller navigated away or hit cancel; nothing to report to them.
throw;
}
catch (Exception ex)
{
// Anything unforeseen still has to arrive as JSON with a message.
_logger.LogError(ex, "Sheets export failed");
return StatusCode(StatusCodes.Status502BadGateway, new { message = ex.Message });
}
}
}
}
@@ -34,5 +34,29 @@ namespace LaDOSE.Api.Controllers
var stats = await _service.GetMatchStats(ids);
return _mapper.Map<MatchStatsDTO>(stats);
}
/// <summary>
/// The players a versus lookup can report on: everyone with at least one set in a
/// bracket whose game is known, by display name. Not the application users —
/// these are tournament players, the entities set rows point at.
/// </summary>
[HttpGet("Players")]
public async Task<List<PlayerOptionDTO>> GetVersusPlayers()
{
var players = await _service.GetVersusPlayers();
return _mapper.Map<List<PlayerOptionDTO>>(players);
}
/// <summary>
/// Every recorded meeting between two players, across all events, split per game.
/// Sets played in a bracket with no game attached are excluded and only counted in
/// UnknownGameSets. Two identical or unknown ids return an empty breakdown, not an error.
/// </summary>
[HttpGet("Versus/{playerAId}/{playerBId}")]
public async Task<PlayerVersusDTO> GetVersus(int playerAId, int playerBId)
{
var versus = await _service.GetVersus(playerAId, playerBId);
return _mapper.Map<PlayerVersusDTO>(versus);
}
}
}