Compare commits

...

2 Commits

Author SHA1 Message Date
Andre Beging
ba9ba119c2 chore: add docker deployment directory 2026-01-29 10:32:50 +01:00
Andre Beging
7c6bfd891c refactor: split shared models and DTOs 2026-01-29 10:32:33 +01:00
23 changed files with 397 additions and 144 deletions

View File

@@ -25,7 +25,7 @@ ASTRAIN is a dark-mode, mobile-first workout logbook. It runs a Blazor WebAssemb
Build and run the single-container setup:
- `docker compose up --build`
- `docker compose -f docker/docker-compose.yml up --build`
The app will be available at http://localhost:8080

29
docker/Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
# Build API
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY src/ASTRAIN.Api/ASTRAIN.Api.csproj ASTRAIN.Api/
COPY src/ASTRAIN.Shared/ASTRAIN.Shared.csproj ASTRAIN.Shared/
RUN dotnet restore ASTRAIN.Api/ASTRAIN.Api.csproj
COPY src/ASTRAIN.Api/ ASTRAIN.Api/
COPY src/ASTRAIN.Shared/ ASTRAIN.Shared/
RUN dotnet publish ASTRAIN.Api/ASTRAIN.Api.csproj -c Release -o /app/publish
# Build Client
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build-client
WORKDIR /src
COPY src/ASTRAIN.Client/ASTRAIN.Client.csproj ASTRAIN.Client/
COPY src/ASTRAIN.Shared/ASTRAIN.Shared.csproj ASTRAIN.Shared/
RUN dotnet restore ASTRAIN.Client/ASTRAIN.Client.csproj
COPY src/ASTRAIN.Client/ ASTRAIN.Client/
COPY src/ASTRAIN.Shared/ ASTRAIN.Shared/
RUN dotnet publish ASTRAIN.Client/ASTRAIN.Client.csproj -c Release -o /app/client
# Final runtime
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
RUN mkdir -p /app/Data
COPY --from=build /app/publish/ ./
COPY --from=build-client /app/client/wwwroot ./wwwroot
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "ASTRAIN.Api.dll"]

10
docker/docker-compose.yml Normal file
View File

@@ -0,0 +1,10 @@
version: "3.9"
services:
astrain:
build:
context: ..
dockerfile: docker/Dockerfile
ports:
- "8080:8080"
volumes:
- ./data:/app/Data

View File

@@ -1,4 +1,4 @@
using ASTRAIN.Shared;
using ASTRAIN.Shared.Models;
using Microsoft.EntityFrameworkCore;
namespace ASTRAIN.Api.Data;

View File

@@ -1,7 +1,10 @@
using System.Text.RegularExpressions;
using ASTRAIN.Api.Data;
using ASTRAIN.Api.Services;
using ASTRAIN.Shared;
using ASTRAIN.Shared.Dtos;
using ASTRAIN.Shared.Models;
using ASTRAIN.Shared.Requests;
using ASTRAIN.Shared.Responses;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);

View File

@@ -1,5 +1,7 @@
using System.Net.Http.Json;
using ASTRAIN.Shared;
using ASTRAIN.Shared.Dtos;
using ASTRAIN.Shared.Requests;
using ASTRAIN.Shared.Responses;
namespace ASTRAIN.Client.Services;

View File

@@ -9,5 +9,7 @@
@using ASTRAIN.Client
@using ASTRAIN.Client.Layout
@using ASTRAIN.Client.Services
@using ASTRAIN.Shared
@using ASTRAIN.Shared.Dtos
@using ASTRAIN.Shared.Requests
@using ASTRAIN.Shared.Responses
@using ASTRAIN.Client.Pages

View File

@@ -1,139 +1 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ASTRAIN.Shared;
public class User
{
[Key]
[MaxLength(8)]
public string Id { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public List<Exercise> Exercises { get; set; } = new();
public List<Routine> Routines { get; set; } = new();
}
public class Exercise
{
public int Id { get; set; }
[Required]
[MaxLength(120)]
public string Name { get; set; } = string.Empty;
[Required]
[MaxLength(8)]
public string UserId { get; set; } = string.Empty;
public User? User { get; set; }
public List<RoutineExercise> RoutineExercises { get; set; } = new();
}
public class Routine
{
public int Id { get; set; }
[Required]
[MaxLength(120)]
public string Name { get; set; } = string.Empty;
[Required]
[MaxLength(8)]
public string UserId { get; set; } = string.Empty;
public User? User { get; set; }
public List<RoutineExercise> Exercises { get; set; } = new();
public List<RoutineRun> Runs { get; set; } = new();
}
public class RoutineExercise
{
public int Id { get; set; }
[Required]
public int RoutineId { get; set; }
public Routine? Routine { get; set; }
[Required]
public int ExerciseId { get; set; }
public Exercise? Exercise { get; set; }
public int Order { get; set; }
}
public class RoutineRun
{
public int Id { get; set; }
[Required]
public int RoutineId { get; set; }
public Routine? Routine { get; set; }
[Required]
[MaxLength(8)]
public string UserId { get; set; } = string.Empty;
public DateTime PerformedAt { get; set; } = DateTime.UtcNow;
public List<RoutineRunEntry> Entries { get; set; } = new();
}
public class RoutineRunEntry
{
public int Id { get; set; }
[Required]
public int RoutineRunId { get; set; }
public RoutineRun? RoutineRun { get; set; }
[Required]
public int ExerciseId { get; set; }
public Exercise? Exercise { get; set; }
public double Weight { get; set; }
public bool Completed { get; set; }
}
public record EnsureUserResponse(string UserId);
public record ExerciseDto(int Id, string Name);
public record ExerciseUpsertRequest(string Name);
public record RoutineExerciseDto(int ExerciseId, string Name, int Order);
public record RoutineDto(int Id, string Name, List<RoutineExerciseDto> Exercises);
public record RoutineUpsertRequest(string Name, List<int> ExerciseIds);
public class RoutineRunEntryDto
{
public int ExerciseId { get; set; }
public double Weight { get; set; }
public bool Completed { get; set; }
public RoutineRunEntryDto()
{
}
public RoutineRunEntryDto(int exerciseId, double weight, bool completed)
{
ExerciseId = exerciseId;
Weight = weight;
Completed = completed;
}
}
public record RoutineRunRequest(List<RoutineRunEntryDto> Entries);
public record RoutineRunSummaryDto(DateTime PerformedAt, List<RoutineRunEntryDto> Entries);
// Legacy file intentionally left empty. Types were moved into dedicated files.

View File

@@ -0,0 +1,8 @@
namespace ASTRAIN.Shared.Dtos;
/// <summary>
/// Data transfer object for an exercise.
/// </summary>
/// <param name="Id">Exercise identifier.</param>
/// <param name="Name">Exercise name.</param>
public record ExerciseDto(int Id, string Name);

View File

@@ -0,0 +1,9 @@
namespace ASTRAIN.Shared.Dtos;
/// <summary>
/// Data transfer object for a routine and its exercises.
/// </summary>
/// <param name="Id">Routine identifier.</param>
/// <param name="Name">Routine name.</param>
/// <param name="Exercises">Ordered exercises.</param>
public record RoutineDto(int Id, string Name, List<RoutineExerciseDto> Exercises);

View File

@@ -0,0 +1,9 @@
namespace ASTRAIN.Shared.Dtos;
/// <summary>
/// Data transfer object for a routine exercise entry.
/// </summary>
/// <param name="ExerciseId">Exercise identifier.</param>
/// <param name="Name">Exercise name.</param>
/// <param name="Order">Order within the routine.</param>
public record RoutineExerciseDto(int ExerciseId, string Name, int Order);

View File

@@ -0,0 +1,39 @@
namespace ASTRAIN.Shared.Dtos;
/// <summary>
/// Data transfer object for a routine run entry.
/// </summary>
public class RoutineRunEntryDto
{
/// <summary>
/// Gets or sets the exercise identifier.
/// </summary>
public int ExerciseId { get; set; }
/// <summary>
/// Gets or sets the weight used.
/// </summary>
public double Weight { get; set; }
/// <summary>
/// Gets or sets whether the exercise was completed.
/// </summary>
public bool Completed { get; set; }
/// <summary>
/// Initializes a new instance for serialization.
/// </summary>
public RoutineRunEntryDto()
{
}
/// <summary>
/// Initializes a new instance with values.
/// </summary>
public RoutineRunEntryDto(int exerciseId, double weight, bool completed)
{
ExerciseId = exerciseId;
Weight = weight;
Completed = completed;
}
}

View File

@@ -0,0 +1,8 @@
namespace ASTRAIN.Shared.Dtos;
/// <summary>
/// Data transfer object for a routine run summary.
/// </summary>
/// <param name="PerformedAt">Run timestamp in UTC.</param>
/// <param name="Entries">Entries recorded in the run.</param>
public record RoutineRunSummaryDto(DateTime PerformedAt, List<RoutineRunEntryDto> Entries);

View File

@@ -0,0 +1,38 @@
using System.ComponentModel.DataAnnotations;
namespace ASTRAIN.Shared.Models;
/// <summary>
/// Represents a single exercise item (e.g., a machine or movement).
/// </summary>
public class Exercise
{
/// <summary>
/// Gets or sets the database identifier.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the exercise display name.
/// </summary>
[Required]
[MaxLength(120)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the owning user's identifier.
/// </summary>
[Required]
[MaxLength(8)]
public string UserId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the owning user.
/// </summary>
public User? User { get; set; }
/// <summary>
/// Gets or sets routine associations for this exercise.
/// </summary>
public List<RoutineExercise> RoutineExercises { get; set; } = new();
}

View File

@@ -0,0 +1,43 @@
using System.ComponentModel.DataAnnotations;
namespace ASTRAIN.Shared.Models;
/// <summary>
/// Represents a routine consisting of ordered exercises.
/// </summary>
public class Routine
{
/// <summary>
/// Gets or sets the database identifier.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the routine name.
/// </summary>
[Required]
[MaxLength(120)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the owning user's identifier.
/// </summary>
[Required]
[MaxLength(8)]
public string UserId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the owning user.
/// </summary>
public User? User { get; set; }
/// <summary>
/// Gets or sets the ordered exercises in this routine.
/// </summary>
public List<RoutineExercise> Exercises { get; set; } = new();
/// <summary>
/// Gets or sets the run history for this routine.
/// </summary>
public List<RoutineRun> Runs { get; set; } = new();
}

View File

@@ -0,0 +1,41 @@
using System.ComponentModel.DataAnnotations;
namespace ASTRAIN.Shared.Models;
/// <summary>
/// Join entity linking a routine to an exercise with an explicit order.
/// </summary>
public class RoutineExercise
{
/// <summary>
/// Gets or sets the database identifier.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the routine identifier.
/// </summary>
[Required]
public int RoutineId { get; set; }
/// <summary>
/// Gets or sets the routine.
/// </summary>
public Routine? Routine { get; set; }
/// <summary>
/// Gets or sets the exercise identifier.
/// </summary>
[Required]
public int ExerciseId { get; set; }
/// <summary>
/// Gets or sets the exercise.
/// </summary>
public Exercise? Exercise { get; set; }
/// <summary>
/// Gets or sets the display order within the routine.
/// </summary>
public int Order { get; set; }
}

View File

@@ -0,0 +1,42 @@
using System.ComponentModel.DataAnnotations;
namespace ASTRAIN.Shared.Models;
/// <summary>
/// Represents a single execution of a routine.
/// </summary>
public class RoutineRun
{
/// <summary>
/// Gets or sets the database identifier.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the routine identifier.
/// </summary>
[Required]
public int RoutineId { get; set; }
/// <summary>
/// Gets or sets the routine.
/// </summary>
public Routine? Routine { get; set; }
/// <summary>
/// Gets or sets the user identifier for this run.
/// </summary>
[Required]
[MaxLength(8)]
public string UserId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the run timestamp in UTC.
/// </summary>
public DateTime PerformedAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// Gets or sets the entries completed during the run.
/// </summary>
public List<RoutineRunEntry> Entries { get; set; } = new();
}

View File

@@ -0,0 +1,46 @@
using System.ComponentModel.DataAnnotations;
namespace ASTRAIN.Shared.Models;
/// <summary>
/// Represents an exercise entry within a routine run.
/// </summary>
public class RoutineRunEntry
{
/// <summary>
/// Gets or sets the database identifier.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the parent routine run identifier.
/// </summary>
[Required]
public int RoutineRunId { get; set; }
/// <summary>
/// Gets or sets the parent routine run.
/// </summary>
public RoutineRun? RoutineRun { get; set; }
/// <summary>
/// Gets or sets the exercise identifier.
/// </summary>
[Required]
public int ExerciseId { get; set; }
/// <summary>
/// Gets or sets the exercise.
/// </summary>
public Exercise? Exercise { get; set; }
/// <summary>
/// Gets or sets the used weight.
/// </summary>
public double Weight { get; set; }
/// <summary>
/// Gets or sets whether the exercise was completed.
/// </summary>
public bool Completed { get; set; }
}

View File

@@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
namespace ASTRAIN.Shared.Models;
/// <summary>
/// Represents an application user identified by an 8-character key.
/// </summary>
public class User
{
/// <summary>
/// Gets or sets the unique 8-character user identifier.
/// </summary>
[Key]
[MaxLength(8)]
public string Id { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the creation timestamp in UTC.
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// Gets or sets the exercises owned by this user.
/// </summary>
public List<Exercise> Exercises { get; set; } = new();
/// <summary>
/// Gets or sets the routines owned by this user.
/// </summary>
public List<Routine> Routines { get; set; } = new();
}

View File

@@ -0,0 +1,7 @@
namespace ASTRAIN.Shared.Requests;
/// <summary>
/// Request payload to create or update an exercise.
/// </summary>
/// <param name="Name">Exercise name.</param>
public record ExerciseUpsertRequest(string Name);

View File

@@ -0,0 +1,9 @@
using ASTRAIN.Shared.Dtos;
namespace ASTRAIN.Shared.Requests;
/// <summary>
/// Request payload to save a routine run.
/// </summary>
/// <param name="Entries">Run entries.</param>
public record RoutineRunRequest(List<RoutineRunEntryDto> Entries);

View File

@@ -0,0 +1,8 @@
namespace ASTRAIN.Shared.Requests;
/// <summary>
/// Request payload to create or update a routine.
/// </summary>
/// <param name="Name">Routine name.</param>
/// <param name="ExerciseIds">Ordered exercise identifiers.</param>
public record RoutineUpsertRequest(string Name, List<int> ExerciseIds);

View File

@@ -0,0 +1,7 @@
namespace ASTRAIN.Shared.Responses;
/// <summary>
/// Response payload for ensuring a user exists.
/// </summary>
/// <param name="UserId">The resolved user identifier.</param>
public record EnsureUserResponse(string UserId);