Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions AdaptiveRemote.sln
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.Backend.Layo
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.Backend.Common", "src\AdaptiveRemote.Backend.Common\AdaptiveRemote.Backend.Common.csproj", "{1F36A31B-299C-480C-B974-F4CE67C6F034}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.Backend.LayoutCompilerService", "src\AdaptiveRemote.Backend.LayoutCompilerService\AdaptiveRemote.Backend.LayoutCompilerService.csproj", "{42CD5829-E4EC-4135-9575-536F0074D21E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.Backend.LayoutCompilerService.Tests", "test\AdaptiveRemote.Backend.LayoutCompilerService.Tests\AdaptiveRemote.Backend.LayoutCompilerService.Tests.csproj", "{D8D4D86A-7882-4DA3-B35A-0357801D9D4C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -318,6 +322,30 @@ Global
{1F36A31B-299C-480C-B974-F4CE67C6F034}.Release|x64.Build.0 = Release|Any CPU
{1F36A31B-299C-480C-B974-F4CE67C6F034}.Release|x86.ActiveCfg = Release|Any CPU
{1F36A31B-299C-480C-B974-F4CE67C6F034}.Release|x86.Build.0 = Release|Any CPU
{42CD5829-E4EC-4135-9575-536F0074D21E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42CD5829-E4EC-4135-9575-536F0074D21E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42CD5829-E4EC-4135-9575-536F0074D21E}.Debug|x64.ActiveCfg = Debug|Any CPU
{42CD5829-E4EC-4135-9575-536F0074D21E}.Debug|x64.Build.0 = Debug|Any CPU
{42CD5829-E4EC-4135-9575-536F0074D21E}.Debug|x86.ActiveCfg = Debug|Any CPU
{42CD5829-E4EC-4135-9575-536F0074D21E}.Debug|x86.Build.0 = Debug|Any CPU
{42CD5829-E4EC-4135-9575-536F0074D21E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42CD5829-E4EC-4135-9575-536F0074D21E}.Release|Any CPU.Build.0 = Release|Any CPU
{42CD5829-E4EC-4135-9575-536F0074D21E}.Release|x64.ActiveCfg = Release|Any CPU
{42CD5829-E4EC-4135-9575-536F0074D21E}.Release|x64.Build.0 = Release|Any CPU
{42CD5829-E4EC-4135-9575-536F0074D21E}.Release|x86.ActiveCfg = Release|Any CPU
{42CD5829-E4EC-4135-9575-536F0074D21E}.Release|x86.Build.0 = Release|Any CPU
{D8D4D86A-7882-4DA3-B35A-0357801D9D4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D8D4D86A-7882-4DA3-B35A-0357801D9D4C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D8D4D86A-7882-4DA3-B35A-0357801D9D4C}.Debug|x64.ActiveCfg = Debug|Any CPU
{D8D4D86A-7882-4DA3-B35A-0357801D9D4C}.Debug|x64.Build.0 = Debug|Any CPU
{D8D4D86A-7882-4DA3-B35A-0357801D9D4C}.Debug|x86.ActiveCfg = Debug|Any CPU
{D8D4D86A-7882-4DA3-B35A-0357801D9D4C}.Debug|x86.Build.0 = Debug|Any CPU
{D8D4D86A-7882-4DA3-B35A-0357801D9D4C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D8D4D86A-7882-4DA3-B35A-0357801D9D4C}.Release|Any CPU.Build.0 = Release|Any CPU
{D8D4D86A-7882-4DA3-B35A-0357801D9D4C}.Release|x64.ActiveCfg = Release|Any CPU
{D8D4D86A-7882-4DA3-B35A-0357801D9D4C}.Release|x64.Build.0 = Release|Any CPU
{D8D4D86A-7882-4DA3-B35A-0357801D9D4C}.Release|x86.ActiveCfg = Release|Any CPU
{D8D4D86A-7882-4DA3-B35A-0357801D9D4C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -340,6 +368,8 @@ Global
{F341B9BA-8517-447F-93B3-7E09AAD0F0E1} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{A829A88B-C42D-4D3B-8CDE-621862E4B39C} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
{1F36A31B-299C-480C-B974-F4CE67C6F034} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{42CD5829-E4EC-4135-9575-536F0074D21E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{D8D4D86A-7882-4DA3-B35A-0357801D9D4C} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {556A11E4-2F89-4600-9831-8162F067EC3E}
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ Event IDs are organized in ranges by subsystem:
| 800–899 | TiVoConnection |
| 900–999 | UdpService |
| 1000–1099 | BroadlinkCommandService |
| 1100–1199 | CompiledLayoutService (backend) |
| 1100–1179 | CompiledLayoutService (backend) |
| 1180–1199 | LayoutCompilerService (backend) |
| 1200–1299 | RawLayoutService (backend) |
| 1300–1699 | (reserved — App subsystems: ProgrammaticSettings, ScopedBackgroundProcess, ConversationState, SamplesRecorder, TestEndpointService, CognitoTokenService) |
| 1700–1799 | LayoutProcessingService (backend) |
Expand Down
2 changes: 2 additions & 0 deletions backend.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
"src\\AdaptiveRemote.Backend.CompiledLayoutService\\AdaptiveRemote.Backend.CompiledLayoutService.csproj",
"src\\AdaptiveRemote.Backend.RawLayoutService\\AdaptiveRemote.Backend.RawLayoutService.csproj",
"src\\AdaptiveRemote.Backend.LayoutProcessingService\\AdaptiveRemote.Backend.LayoutProcessingService.csproj",
"src\\AdaptiveRemote.Backend.LayoutCompilerService\\AdaptiveRemote.Backend.LayoutCompilerService.csproj",
"test\\AdaptiveRemote.Backend.ApiTests\\AdaptiveRemote.Backend.ApiTests.csproj",
"test\\AdaptiveRemote.Backend.RawLayoutService.Tests\\AdaptiveRemote.Backend.RawLayoutService.Tests.csproj",
"test\\AdaptiveRemote.Backend.LayoutProcessingService.Tests\\AdaptiveRemote.Backend.LayoutProcessingService.Tests.csproj",
"test\\AdaptiveRemote.Backend.LayoutCompilerService.Tests\\AdaptiveRemote.Backend.LayoutCompilerService.Tests.csproj",
"test\\AdaptiveRemote.TestUtilities\\AdaptiveRemote.TestUtilities.csproj"
]
}
Expand Down
14 changes: 14 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ services:
networks:
- backend

layoutcompilerservice:
build:
context: .
dockerfile: src/AdaptiveRemote.Backend.LayoutCompilerService/Dockerfile
ports:
- "8083:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://+:8080
networks:
- backend

layoutprocessingservice:
build:
context: .
Expand All @@ -81,13 +93,15 @@ services:
# Upstream service URLs (Docker Compose DNS)
- RawLayoutService__BaseUrl=http://rawlayoutservice:8080
- CompiledLayoutService__BaseUrl=http://compiledlayoutservice:8080
- LayoutCompilerService__BaseUrl=http://layoutcompilerservice:8080
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- LocalStack__BaseUrl=http://localstack:4566
depends_on:
- localstack
- rawlayoutservice
- compiledlayoutservice
- layoutcompilerservice
networks:
- backend

Expand Down
23 changes: 21 additions & 2 deletions src/AdaptiveRemote.Backend.Common/Logging/MessageLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
namespace AdaptiveRemote.Backend.Common.Logging;

/// <summary>
/// Centralized logging messages for CompiledLayoutService.
/// Centralized logging messages for backend services.
/// All log messages MUST be defined here as [LoggerMessage] source-generated methods.
/// Event ID ranges:
/// 1100-1199: CompiledLayoutService
/// 1100-1199: shared backend messages (1100-1107: common; 1180-1199: LayoutCompilerService)
/// 1200-1299: RawLayoutService
/// 1300-1399: CompiledLayoutService
/// 1700-1799: LayoutProcessingService
/// </summary>
public static partial class MessageLogger
{
Expand Down Expand Up @@ -38,6 +41,22 @@ public static partial class MessageLogger
Message = "LocalStack dependency check failed at {HealthUrl}: {FailureReason}. LocalStack is required for local development. See docs/local-dev.md for setup instructions")]
public static partial void LocalStackDependencyUnavailable(this ILogger logger, string healthUrl, string failureReason, Exception? exception);

// LayoutCompilerService-specific messages
[LoggerMessage(EventId = 1180, Level = LogLevel.Information, Message = "Compilation started; rawLayoutId={RawLayoutId} elementCount={ElementCount}")]
public static partial void CompilationStarted(this ILogger logger, Guid rawLayoutId, int elementCount);

[LoggerMessage(EventId = 1181, Level = LogLevel.Information, Message = "Compilation succeeded; rawLayoutId={RawLayoutId} compiledElementCount={CompiledElementCount}")]
public static partial void CompilationSucceeded(this ILogger logger, Guid rawLayoutId, int compiledElementCount);

[LoggerMessage(EventId = 1182, Level = LogLevel.Information, Message = "Preview compilation started; elementCount={ElementCount}")]
public static partial void PreviewCompilationStarted(this ILogger logger, int elementCount);

[LoggerMessage(EventId = 1183, Level = LogLevel.Information, Message = "Preview compilation succeeded; elementCount={ElementCount}")]
public static partial void PreviewCompilationSucceeded(this ILogger logger, int elementCount);

[LoggerMessage(EventId = 1184, Level = LogLevel.Error, Message = "Compilation failed; rawLayoutId={RawLayoutId} reason={Reason}")]
public static partial void CompilationFailed(this ILogger logger, Guid rawLayoutId, string reason, Exception? exception);

// CompiledLayoutService-specific messages
[LoggerMessage(EventId = 1301, Level = LogLevel.Information, Message = "Returning active compiled layout Id={LayoutId}")]
public static partial void ReturningActiveLayout(this ILogger logger, Guid layoutId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>AdaptiveRemote.Backend.LayoutCompilerService</RootNamespace>
<PublishAot>true</PublishAot>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\AdaptiveRemote.Backend.Common\AdaptiveRemote.Backend.Common.csproj" />
<ProjectReference Include="..\AdaptiveRemote.Contracts\AdaptiveRemote.Contracts.csproj" />
</ItemGroup>

</Project>
26 changes: 26 additions & 0 deletions src/AdaptiveRemote.Backend.LayoutCompilerService/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

# Copy csproj files and restore
COPY ["src/AdaptiveRemote.Contracts/AdaptiveRemote.Contracts.csproj", "AdaptiveRemote.Contracts/"]
COPY ["src/AdaptiveRemote.Backend.Common/AdaptiveRemote.Backend.Common.csproj", "AdaptiveRemote.Backend.Common/"]
COPY ["src/AdaptiveRemote.Backend.LayoutCompilerService/AdaptiveRemote.Backend.LayoutCompilerService.csproj", "AdaptiveRemote.Backend.LayoutCompilerService/"]
COPY ["Directory.Build.props", "./"]
COPY ["Directory.Packages.props", "./"]
RUN dotnet restore "AdaptiveRemote.Backend.LayoutCompilerService/AdaptiveRemote.Backend.LayoutCompilerService.csproj"

# Copy source and build
COPY ["src/AdaptiveRemote.Contracts/", "AdaptiveRemote.Contracts/"]
COPY ["src/AdaptiveRemote.Backend.Common/", "AdaptiveRemote.Backend.Common/"]
COPY ["src/AdaptiveRemote.Backend.LayoutCompilerService/", "AdaptiveRemote.Backend.LayoutCompilerService/"]
WORKDIR "/src/AdaptiveRemote.Backend.LayoutCompilerService"
RUN dotnet build "AdaptiveRemote.Backend.LayoutCompilerService.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "AdaptiveRemote.Backend.LayoutCompilerService.csproj" -c Release -o /app/publish /p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=publish /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "AdaptiveRemote.Backend.LayoutCompilerService.dll"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using System.Text.Json;
using AdaptiveRemote.Backend.Common.Logging;
using AdaptiveRemote.Contracts;

namespace AdaptiveRemote.Backend.LayoutCompilerService.Endpoints;

public static class CompileEndpoints
{
public static void MapCompileEndpoints(this IEndpointRouteBuilder app)
{
app.MapPost("/compile", CompileLayout)
.WithName(nameof(CompileLayout))
.Produces<CompiledLayout>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest);

app.MapPost("/compile/preview", CompilePreview)
.WithName(nameof(CompilePreview))
.Produces<PreviewLayout>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest);
}

private static async Task<IResult> CompileLayout(
HttpContext httpContext,
ILogger<Program> logger,
CancellationToken cancellationToken)
{
using IDisposable _ = logger.StartRequestScope("POST", "/compile");

RawLayout? raw;
try
{
raw = await JsonSerializer
.DeserializeAsync(httpContext.Request.Body, LayoutContractsJsonContext.Default.RawLayout, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception ex)
{
logger.CompilationFailed(Guid.Empty, $"Failed to deserialize request body: {ex.Message}", ex);
return Results.BadRequest("Invalid request body: could not deserialize RawLayout.");
}

if (raw is null)
{
logger.CompilationFailed(Guid.Empty, "Request body deserialized to null", exception: null);
return Results.BadRequest("Invalid request body: RawLayout was null.");
}

logger.CompilationStarted(raw.Id, raw.Elements.Count);

CompiledLayout compiled;
try
{
compiled = LayoutCompilationEngine.Compile(raw);
}
catch (Exception ex)
{
logger.CompilationFailed(raw.Id, ex.Message, ex);
return Results.BadRequest("Compilation failed.");
}
Comment on lines +55 to +59

logger.CompilationSucceeded(raw.Id, compiled.Elements.Count);

string responseJson = JsonSerializer.Serialize(compiled, LayoutContractsJsonContext.Default.CompiledLayout);
return Results.Content(responseJson, "application/json");
}

private static async Task<IResult> CompilePreview(
HttpContext httpContext,
ILogger<Program> logger,
CancellationToken cancellationToken)
{
using IDisposable _ = logger.StartRequestScope("POST", "/compile/preview");

IReadOnlyList<RawLayoutElementDto>? elements;
try
{
elements = await JsonSerializer
.DeserializeAsync(httpContext.Request.Body, LayoutContractsJsonContext.Default.IReadOnlyListRawLayoutElementDto, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception ex)
{
logger.CompilationFailed(Guid.Empty, $"Failed to deserialize preview request body: {ex.Message}", ex);
return Results.BadRequest("Invalid request body: could not deserialize element list.");
}

if (elements is null)
{
logger.CompilationFailed(Guid.Empty, "Preview request body deserialized to null", exception: null);
return Results.BadRequest("Invalid request body: element list was null.");
}

logger.PreviewCompilationStarted(elements.Count);

PreviewLayout preview;
try
{
preview = LayoutCompilationEngine.CompilePreview(elements);
}
catch (Exception ex)
{
logger.CompilationFailed(Guid.Empty, $"Preview compilation failed: {ex.Message}", ex);
return Results.BadRequest("Preview compilation failed.");
}
Comment on lines +100 to +104

logger.PreviewCompilationSucceeded(elements.Count);

string responseJson = JsonSerializer.Serialize(preview, LayoutContractsJsonContext.Default.PreviewLayout);
return Results.Content(responseJson, "application/json");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("AdaptiveRemote.Backend.LayoutCompilerService.Tests")]
Loading