diff --git a/.gitignore b/.gitignore index 6303abf..c82c377 100644 --- a/.gitignore +++ b/.gitignore @@ -254,6 +254,9 @@ paket-files/ .idea/ *.sln.iml +# Docker environment variables +.env + # CodeRush .cr/ diff --git a/Chrono/.dockerignore b/Chrono/.dockerignore new file mode 100644 index 0000000..941d7f5 --- /dev/null +++ b/Chrono/.dockerignore @@ -0,0 +1,9 @@ +**/.git +**/.obj +**/.bin +**/.vs +**/.vscode +**/.idea +bin/ +obj/ +publish/ diff --git a/Chrono/.env.template b/Chrono/.env.template new file mode 100644 index 0000000..600b9d8 --- /dev/null +++ b/Chrono/.env.template @@ -0,0 +1,9 @@ +# Telerik License Key +# Get your license key from: https://www.telerik.com/kendo-blazor-ui/my-license/ +# This is a long signed string, different from your NuGet password. +TELERIK_LICENSE=your_telerik_license_key_here + +# Telerik NuGet Credentials +# Use 'api-key' for Username if using an API Key +TELERIK_USERNAME=your_telerik_username_or_api-key +TELERIK_PASSWORD=your_telerik_password_or_api_key diff --git a/Chrono/Chrono.sln b/Chrono/Chrono.sln index 95e905f..458e9a4 100644 --- a/Chrono/Chrono.sln +++ b/Chrono/Chrono.sln @@ -10,31 +10,92 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deploy", "Deploy\Deploy.csp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{90F32056-6983-4224-8A8C-E797C71633F3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{BFB7B73F-EFDD-4DE8-89F2-E1CBE1B55C27}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {18981096-443A-44BF-AE56-6499C2B03AEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {18981096-443A-44BF-AE56-6499C2B03AEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18981096-443A-44BF-AE56-6499C2B03AEF}.Debug|x64.ActiveCfg = Debug|Any CPU + {18981096-443A-44BF-AE56-6499C2B03AEF}.Debug|x64.Build.0 = Debug|Any CPU + {18981096-443A-44BF-AE56-6499C2B03AEF}.Debug|x86.ActiveCfg = Debug|Any CPU + {18981096-443A-44BF-AE56-6499C2B03AEF}.Debug|x86.Build.0 = Debug|Any CPU {18981096-443A-44BF-AE56-6499C2B03AEF}.Release|Any CPU.ActiveCfg = Release|Any CPU {18981096-443A-44BF-AE56-6499C2B03AEF}.Release|Any CPU.Build.0 = Release|Any CPU + {18981096-443A-44BF-AE56-6499C2B03AEF}.Release|x64.ActiveCfg = Release|Any CPU + {18981096-443A-44BF-AE56-6499C2B03AEF}.Release|x64.Build.0 = Release|Any CPU + {18981096-443A-44BF-AE56-6499C2B03AEF}.Release|x86.ActiveCfg = Release|Any CPU + {18981096-443A-44BF-AE56-6499C2B03AEF}.Release|x86.Build.0 = Release|Any CPU {36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Debug|x64.ActiveCfg = Debug|Any CPU + {36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Debug|x64.Build.0 = Debug|Any CPU + {36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Debug|x86.ActiveCfg = Debug|Any CPU + {36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Debug|x86.Build.0 = Debug|Any CPU {36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Release|Any CPU.ActiveCfg = Release|Any CPU {36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Release|Any CPU.Build.0 = Release|Any CPU + {36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Release|x64.ActiveCfg = Release|Any CPU + {36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Release|x64.Build.0 = Release|Any CPU + {36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Release|x86.ActiveCfg = Release|Any CPU + {36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Release|x86.Build.0 = Release|Any CPU {3358AF7A-603B-4BAC-A7F5-07978FB87843}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3358AF7A-603B-4BAC-A7F5-07978FB87843}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3358AF7A-603B-4BAC-A7F5-07978FB87843}.Debug|x64.ActiveCfg = Debug|Any CPU + {3358AF7A-603B-4BAC-A7F5-07978FB87843}.Debug|x64.Build.0 = Debug|Any CPU + {3358AF7A-603B-4BAC-A7F5-07978FB87843}.Debug|x86.ActiveCfg = Debug|Any CPU + {3358AF7A-603B-4BAC-A7F5-07978FB87843}.Debug|x86.Build.0 = Debug|Any CPU {3358AF7A-603B-4BAC-A7F5-07978FB87843}.Release|Any CPU.ActiveCfg = Release|Any CPU {3358AF7A-603B-4BAC-A7F5-07978FB87843}.Release|Any CPU.Build.0 = Release|Any CPU + {3358AF7A-603B-4BAC-A7F5-07978FB87843}.Release|x64.ActiveCfg = Release|Any CPU + {3358AF7A-603B-4BAC-A7F5-07978FB87843}.Release|x64.Build.0 = Release|Any CPU + {3358AF7A-603B-4BAC-A7F5-07978FB87843}.Release|x86.ActiveCfg = Release|Any CPU + {3358AF7A-603B-4BAC-A7F5-07978FB87843}.Release|x86.Build.0 = Release|Any CPU {E5E9A799-76C8-43B3-B9C2-3CAEC2B5E1DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E5E9A799-76C8-43B3-B9C2-3CAEC2B5E1DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5E9A799-76C8-43B3-B9C2-3CAEC2B5E1DD}.Debug|x64.ActiveCfg = Debug|Any CPU + {E5E9A799-76C8-43B3-B9C2-3CAEC2B5E1DD}.Debug|x64.Build.0 = Debug|Any CPU + {E5E9A799-76C8-43B3-B9C2-3CAEC2B5E1DD}.Debug|x86.ActiveCfg = Debug|Any CPU + {E5E9A799-76C8-43B3-B9C2-3CAEC2B5E1DD}.Debug|x86.Build.0 = Debug|Any CPU {E5E9A799-76C8-43B3-B9C2-3CAEC2B5E1DD}.Release|Any CPU.ActiveCfg = Release|Any CPU {E5E9A799-76C8-43B3-B9C2-3CAEC2B5E1DD}.Release|Any CPU.Build.0 = Release|Any CPU + {E5E9A799-76C8-43B3-B9C2-3CAEC2B5E1DD}.Release|x64.ActiveCfg = Release|Any CPU + {E5E9A799-76C8-43B3-B9C2-3CAEC2B5E1DD}.Release|x64.Build.0 = Release|Any CPU + {E5E9A799-76C8-43B3-B9C2-3CAEC2B5E1DD}.Release|x86.ActiveCfg = Release|Any CPU + {E5E9A799-76C8-43B3-B9C2-3CAEC2B5E1DD}.Release|x86.Build.0 = Release|Any CPU {90F32056-6983-4224-8A8C-E797C71633F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {90F32056-6983-4224-8A8C-E797C71633F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90F32056-6983-4224-8A8C-E797C71633F3}.Debug|x64.ActiveCfg = Debug|Any CPU + {90F32056-6983-4224-8A8C-E797C71633F3}.Debug|x64.Build.0 = Debug|Any CPU + {90F32056-6983-4224-8A8C-E797C71633F3}.Debug|x86.ActiveCfg = Debug|Any CPU + {90F32056-6983-4224-8A8C-E797C71633F3}.Debug|x86.Build.0 = Debug|Any CPU {90F32056-6983-4224-8A8C-E797C71633F3}.Release|Any CPU.ActiveCfg = Release|Any CPU {90F32056-6983-4224-8A8C-E797C71633F3}.Release|Any CPU.Build.0 = Release|Any CPU + {90F32056-6983-4224-8A8C-E797C71633F3}.Release|x64.ActiveCfg = Release|Any CPU + {90F32056-6983-4224-8A8C-E797C71633F3}.Release|x64.Build.0 = Release|Any CPU + {90F32056-6983-4224-8A8C-E797C71633F3}.Release|x86.ActiveCfg = Release|Any CPU + {90F32056-6983-4224-8A8C-E797C71633F3}.Release|x86.Build.0 = Release|Any CPU + {BFB7B73F-EFDD-4DE8-89F2-E1CBE1B55C27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BFB7B73F-EFDD-4DE8-89F2-E1CBE1B55C27}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BFB7B73F-EFDD-4DE8-89F2-E1CBE1B55C27}.Debug|x64.ActiveCfg = Debug|Any CPU + {BFB7B73F-EFDD-4DE8-89F2-E1CBE1B55C27}.Debug|x64.Build.0 = Debug|Any CPU + {BFB7B73F-EFDD-4DE8-89F2-E1CBE1B55C27}.Debug|x86.ActiveCfg = Debug|Any CPU + {BFB7B73F-EFDD-4DE8-89F2-E1CBE1B55C27}.Debug|x86.Build.0 = Debug|Any CPU + {BFB7B73F-EFDD-4DE8-89F2-E1CBE1B55C27}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BFB7B73F-EFDD-4DE8-89F2-E1CBE1B55C27}.Release|Any CPU.Build.0 = Release|Any CPU + {BFB7B73F-EFDD-4DE8-89F2-E1CBE1B55C27}.Release|x64.ActiveCfg = Release|Any CPU + {BFB7B73F-EFDD-4DE8-89F2-E1CBE1B55C27}.Release|x64.Build.0 = Release|Any CPU + {BFB7B73F-EFDD-4DE8-89F2-E1CBE1B55C27}.Release|x86.ActiveCfg = Release|Any CPU + {BFB7B73F-EFDD-4DE8-89F2-E1CBE1B55C27}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection EndGlobal diff --git a/Chrono/Dockerfile b/Chrono/Dockerfile new file mode 100644 index 0000000..a1db9d4 --- /dev/null +++ b/Chrono/Dockerfile @@ -0,0 +1,37 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG TELERIK_LICENSE +ARG TELERIK_USERNAME +ARG TELERIK_PASSWORD +ENV TELERIK_LICENSE=$TELERIK_LICENSE +ENV TELERIK_USERNAME=$TELERIK_USERNAME +ENV TELERIK_PASSWORD=$TELERIK_PASSWORD +WORKDIR /src + +# Copy the entire parent directory into the build context +# We expect Chrono and chrono.docs to be siblings +COPY Chrono/ Chrono/ +COPY chrono.docs/ chrono.docs/ + +WORKDIR /src/Chrono +# Ensure Telerik credentials are set and not placeholders +RUN if [ -z "$TELERIK_USERNAME" ] || [ "$TELERIK_USERNAME" = "your_telerik_username_or_api-key" ]; then \ + echo "ERROR: TELERIK_USERNAME is not set in the build environment."; \ + exit 1; \ + fi + +# Inject credentials into nuget.config to ensure they are available for restore +RUN dotnet nuget update source TelerikServer \ + --username "$TELERIK_USERNAME" \ + --password "$TELERIK_PASSWORD" \ + --store-password-in-clear-text \ + --configfile nuget.config + +RUN dotnet restore "Chrono.sln" --configfile nuget.config +RUN dotnet publish "Server/Server.csproj" -c Release -o /app/publish /p:TelerikLicense="$TELERIK_LICENSE" + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final +WORKDIR /app +COPY --from=build /app/publish . +EXPOSE 8080 +ENV ASPNETCORE_URLS=http://+:8080 +ENTRYPOINT ["dotnet", "Server.dll"] diff --git a/Chrono/Model/CardNote.cs b/Chrono/Model/CardNote.cs new file mode 100644 index 0000000..e6680f9 --- /dev/null +++ b/Chrono/Model/CardNote.cs @@ -0,0 +1,7 @@ +namespace Chrono.Model; + +public class CardNote +{ + public string CardName { get; set; } = ""; + public string Note { get; set; } = ""; +} diff --git a/Chrono/Server/AppDbContext.cs b/Chrono/Server/AppDbContext.cs new file mode 100644 index 0000000..bf9287d --- /dev/null +++ b/Chrono/Server/AppDbContext.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using Chrono.Model; + +namespace Server; + +public class AppDbContext : DbContext +{ + public AppDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet CardNotes { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasKey(n => n.CardName); + } +} diff --git a/Chrono/Server/Controllers/NotesController.cs b/Chrono/Server/Controllers/NotesController.cs new file mode 100644 index 0000000..bcb8860 --- /dev/null +++ b/Chrono/Server/Controllers/NotesController.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Chrono.Model; + +namespace Server.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class NotesController : ControllerBase +{ + private readonly AppDbContext _context; + + public NotesController(AppDbContext context) + { + _context = context; + } + + [HttpGet("{cardName}")] + public async Task> GetNote(string cardName) + { + var note = await _context.CardNotes.FindAsync(cardName); + if (note == null) + { + return Ok(new CardNote { CardName = cardName, Note = "" }); + } + return Ok(note); + } + + [HttpPost] + public async Task> UpdateNote(CardNote note) + { + var existing = await _context.CardNotes.FindAsync(note.CardName); + if (existing == null) + { + _context.CardNotes.Add(note); + } + else + { + existing.Note = note.Note; + } + + await _context.SaveChangesAsync(); + return Ok(note); + } +} diff --git a/Chrono/Server/Migrations/20260618210058_InitialCreate.Designer.cs b/Chrono/Server/Migrations/20260618210058_InitialCreate.Designer.cs new file mode 100644 index 0000000..560f721 --- /dev/null +++ b/Chrono/Server/Migrations/20260618210058_InitialCreate.Designer.cs @@ -0,0 +1,43 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Server; + +#nullable disable + +namespace Server.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260618210058_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Chrono.Model.CardNote", b => + { + b.Property("CardName") + .HasColumnType("text"); + + b.Property("Note") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("CardName"); + + b.ToTable("CardNotes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Chrono/Server/Migrations/20260618210058_InitialCreate.cs b/Chrono/Server/Migrations/20260618210058_InitialCreate.cs new file mode 100644 index 0000000..636f6a7 --- /dev/null +++ b/Chrono/Server/Migrations/20260618210058_InitialCreate.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Server.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CardNotes", + columns: table => new + { + CardName = table.Column(type: "text", nullable: false), + Note = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CardNotes", x => x.CardName); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CardNotes"); + } + } +} diff --git a/Chrono/Server/Migrations/AppDbContextModelSnapshot.cs b/Chrono/Server/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..17a337d --- /dev/null +++ b/Chrono/Server/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,40 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Server; + +#nullable disable + +namespace Server.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Chrono.Model.CardNote", b => + { + b.Property("CardName") + .HasColumnType("text"); + + b.Property("Note") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("CardName"); + + b.ToTable("CardNotes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Chrono/Server/Program.cs b/Chrono/Server/Program.cs new file mode 100644 index 0000000..70335a6 --- /dev/null +++ b/Chrono/Server/Program.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore; +using Server; + +var builder = WebApplication.CreateBuilder(args); +builder.WebHost.UseStaticWebAssets(); + +// Add services to the container. +builder.Services.AddControllersWithViews(); +builder.Services.AddRazorPages(); + +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); +builder.Services.AddDbContext(options => + options.UseNpgsql(connectionString)); + +var app = builder.Build(); + +// Apply migrations +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); +} + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseWebAssemblyDebugging(); +} +else +{ + app.UseExceptionHandler("/Error"); +} + +app.UseBlazorFrameworkFiles(); +app.UseStaticFiles(); + +app.UseRouting(); + +app.MapRazorPages(); +app.MapControllers(); +app.MapFallbackToFile("index.html"); + +app.Run(); diff --git a/Chrono/Server/Properties/launchSettings.json b/Chrono/Server/Properties/launchSettings.json new file mode 100644 index 0000000..8f4a30b --- /dev/null +++ b/Chrono/Server/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5256", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7266;http://localhost:5256", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Chrono/Server/Server.csproj b/Chrono/Server/Server.csproj new file mode 100644 index 0000000..3d91b74 --- /dev/null +++ b/Chrono/Server/Server.csproj @@ -0,0 +1,27 @@ + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + net10.0 + enable + enable + + + diff --git a/Chrono/Server/Server.http b/Chrono/Server/Server.http new file mode 100644 index 0000000..3dab34c --- /dev/null +++ b/Chrono/Server/Server.http @@ -0,0 +1,6 @@ +@Server_HostAddress = http://localhost:5256 + +GET {{Server_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Chrono/Server/appsettings.Development.json b/Chrono/Server/appsettings.Development.json new file mode 100644 index 0000000..1972608 --- /dev/null +++ b/Chrono/Server/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Database=chrono;Username=postgres;Password=postgres" + } +} diff --git a/Chrono/Server/appsettings.json b/Chrono/Server/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Chrono/Server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Chrono/Tests/PlaywrightTests.cs b/Chrono/Tests/PlaywrightTests.cs new file mode 100644 index 0000000..9705b26 --- /dev/null +++ b/Chrono/Tests/PlaywrightTests.cs @@ -0,0 +1,51 @@ +using Microsoft.Playwright.NUnit; +using Microsoft.Playwright; + +namespace Tests; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class PlaywrightTests : PageTest +{ + [Test] + public async Task CanWriteAndSaveNote() + { + // 1. Navigate to the cards gallery + await Page.GotoAsync("http://localhost:8080/cards"); + + // 2. Wait for cards to load - looking for at least one card-cell + await Expect(Page.Locator(".card-cell").First).ToBeVisibleAsync(); + + // 3. Find an Agent card and click it. + var agentCard = Page.Locator(".card-cell:has(.card-category-badge.agent)").First; + await agentCard.ClickAsync(); + + // 4. Wait for the detail view to show the note textarea + var noteInput = Page.Locator(".note-input"); + await Expect(noteInput).ToBeVisibleAsync(); + + // 5. Type a unique note + string uniqueNote = "Test note " + Guid.NewGuid().ToString(); + await noteInput.FillAsync(uniqueNote); + + // 6. Blur to trigger save + await noteInput.BlurAsync(); + + // 7. Wait for saving indicator to disappear (if it appeared) + var savingIndicator = Page.Locator(".saving-indicator"); + if (await savingIndicator.IsVisibleAsync()) + { + await Expect(savingIndicator).Not.ToBeVisibleAsync(); + } + + // 8. Close the detail view by clicking the backdrop + await Page.Locator(".modal-backdrop").ClickAsync(); + await Expect(noteInput).Not.ToBeVisibleAsync(); + + // 9. Re-open the same agent card + await agentCard.ClickAsync(); + + // 10. Verify the note is still there + await Expect(noteInput).ToHaveValueAsync(uniqueNote); + } +} diff --git a/Chrono/Tests/TelerikLicenseTests.cs b/Chrono/Tests/TelerikLicenseTests.cs new file mode 100644 index 0000000..6bfb9f4 --- /dev/null +++ b/Chrono/Tests/TelerikLicenseTests.cs @@ -0,0 +1,31 @@ +using Microsoft.Playwright.NUnit; +using Microsoft.Playwright; + +namespace Tests; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class TelerikLicenseTests : PageTest +{ + [Test] + public async Task TelerikLicenseBannerIsNotVisible() + { + // 1. Navigate to the agents page which uses Telerik components + await Page.GotoAsync("http://localhost:8080/agents"); + + // 2. Wait for the page to load and the grid to be visible + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + var grid = Page.Locator(".agents-grid"); + await Expect(grid).ToBeVisibleAsync(); + + // 3. Verify that the Telerik license warning banner is NOT present + // According to Telerik docs, the warning text is: + // "We couldn't verify your license key for Telerik UI for Blazor. Please see the build log for details and resolution steps" + var licenseWarning = Page.GetByText("We couldn't verify your license key for Telerik UI for Blazor"); + await Expect(licenseWarning).Not.ToBeVisibleAsync(); + + // 4. Also verify that no "Trial" banner is visible + var trialBanner = Page.GetByText("Telerik UI for Blazor Trial", new() { Exact = false }); + await Expect(trialBanner).Not.ToBeVisibleAsync(); + } +} diff --git a/Chrono/Web/Pages/Cards.razor b/Chrono/Web/Pages/Cards.razor index f7780fb..94bc7de 100644 --- a/Chrono/Web/Pages/Cards.razor +++ b/Chrono/Web/Pages/Cards.razor @@ -1,4 +1,5 @@ @page "/cards" +@inject HttpClient Http Card Gallery @@ -211,6 +212,19 @@ @selectedCard.ImmortalizeFrom } + + @if (selectedCard.IsAgent) + { +
+ Personal Note + + @if (isSaving) + { + Saving... + } +
+ } @@ -225,6 +239,8 @@ private string costFilter = ""; private CardData? selectedCard; private List factions = []; + private string currentNote = ""; + private bool isSaving = false; private bool HasActiveFilters => categoryFilter != "" || factionFilter != "" || costFilter != ""; @@ -276,9 +292,41 @@ search = ""; } - private void SelectCard(CardData card) + private async Task SelectCard(CardData card) { selectedCard = card; + if (card.IsAgent) + { + currentNote = ""; + try + { + var note = await Http.GetFromJsonAsync($"api/notes/{Uri.EscapeDataString(card.Name)}"); + currentNote = note?.Note ?? ""; + } + catch + { + currentNote = ""; + } + } + } + + private async Task SaveNote() + { + if (selectedCard == null || !selectedCard.IsAgent) return; + + isSaving = true; + try + { + await Http.PostAsJsonAsync("api/notes", new CardNote { CardName = selectedCard.Name, Note = currentNote }); + } + catch + { + // Error handling + } + finally + { + isSaving = false; + } } private void CloseDetail() diff --git a/Chrono/Web/Web.csproj b/Chrono/Web/Web.csproj index e4e3740..7660786 100644 --- a/Chrono/Web/Web.csproj +++ b/Chrono/Web/Web.csproj @@ -4,13 +4,13 @@ net10.0 enable enable - true + false - + diff --git a/Chrono/Web/wwwroot/css/app.css b/Chrono/Web/wwwroot/css/app.css index 244e500..14a0087 100644 --- a/Chrono/Web/wwwroot/css/app.css +++ b/Chrono/Web/wwwroot/css/app.css @@ -225,3 +225,26 @@ a, .btn-link { .loading-status::after { content: var(--blazor-load-percentage-text, "Loading..."); } + +.card-detail .detail-field.note { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.note-input { + min-height: 100px; + resize: vertical; + background: var(--bg-primary); + border: 1px solid var(--border); +} + +.saving-indicator { + font-size: 0.75rem; + color: var(--text-muted); + font-style: italic; + align-self: flex-end; +} diff --git a/Chrono/Web/wwwroot/index.html b/Chrono/Web/wwwroot/index.html index df8a80d..3488404 100644 --- a/Chrono/Web/wwwroot/index.html +++ b/Chrono/Web/wwwroot/index.html @@ -10,13 +10,12 @@ - - - - - - - + + + + + + @@ -46,7 +45,7 @@ Reload 🗙 - + diff --git a/Chrono/chrono.tasks b/Chrono/chrono.tasks new file mode 100644 index 0000000..eb93f9f --- /dev/null +++ b/Chrono/chrono.tasks @@ -0,0 +1,47 @@ +# Chrono CCG - Project Guide + +This project is a hosted Blazor WebAssembly application with a PostgreSQL database for persisting agent notes. + +## Prerequisites +- **Docker Desktop**: Required for the recommended containerized setup. +- **.NET 10 SDK**: Required if you want to build or run the project locally without Docker. + +## 1. Running with Docker (Recommended) +The easiest way to get everything running (App + PostgreSQL) is using Docker Compose. + +1. **Open a terminal** in the project root (`Chrono/`). +2. **Run the following command**: + ```bash + docker-compose up --build + ``` +3. **Access the Application**: + - Web Interface: http://localhost:8080 + - API Endpoint: http://localhost:8080/api/notes + +The database will be automatically initialized and migrations will be applied on startup. + +## 2. Running Locally (Development) +If you need to run the app directly (e.g., for faster debugging): + +1. **Start a PostgreSQL database**. You can use the one from docker-compose if you want: + ```bash + docker-compose up db + ``` +2. **Verify Connection String**: `Server/appsettings.Development.json` is pre-configured to point to `localhost`. +3. **Run the Server project**: + ```bash + cd Server + dotnet run --launch-profile https + ``` +4. The app will be served at the URL shown in the terminal (e.g., https://localhost:7266). + +## 3. Running Tests +To verify the core domain logic: +```bash +dotnet test +``` + +## 4. Key Features +- **Agent Notes**: In the "Cards" gallery, select an Agent to see the "Personal Note" field. Changes are auto-saved to the PostgreSQL database when you click away from the text area. +- **Auto-Migrations**: The Server project automatically handles database schema updates on startup. +- **Dockerized Architecture**: Complete orchestration of the web server and database. diff --git a/Chrono/docker-compose.yml b/Chrono/docker-compose.yml new file mode 100644 index 0000000..bb446dd --- /dev/null +++ b/Chrono/docker-compose.yml @@ -0,0 +1,36 @@ +services: + app: + build: + context: .. + dockerfile: Chrono/Dockerfile + args: + - TELERIK_LICENSE=${TELERIK_LICENSE} + - TELERIK_USERNAME=${TELERIK_USERNAME} + - TELERIK_PASSWORD=${TELERIK_PASSWORD} + ports: + - "8080:8080" + environment: + - TELERIK_LICENSE=${TELERIK_LICENSE} + - ConnectionStrings__DefaultConnection=Host=db;Database=chrono;Username=postgres;Password=postgres + depends_on: + db: + condition: service_healthy + + db: + image: postgres:17-alpine + ports: + - "5432:5432" + environment: + - POSTGRES_DB=chrono + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: diff --git a/Chrono/nuget.config b/Chrono/nuget.config new file mode 100644 index 0000000..63cb604 --- /dev/null +++ b/Chrono/nuget.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/chrono.tasks/.obsidian/app.json b/chrono.tasks/.obsidian/app.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/chrono.tasks/.obsidian/app.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/chrono.tasks/.obsidian/appearance.json b/chrono.tasks/.obsidian/appearance.json new file mode 100644 index 0000000..4be7969 --- /dev/null +++ b/chrono.tasks/.obsidian/appearance.json @@ -0,0 +1,3 @@ +{ + "theme": "obsidian" +} \ No newline at end of file diff --git a/chrono.tasks/.obsidian/core-plugins.json b/chrono.tasks/.obsidian/core-plugins.json new file mode 100644 index 0000000..639b90d --- /dev/null +++ b/chrono.tasks/.obsidian/core-plugins.json @@ -0,0 +1,33 @@ +{ + "file-explorer": true, + "global-search": true, + "switcher": true, + "graph": true, + "backlink": true, + "canvas": true, + "outgoing-link": true, + "tag-pane": true, + "footnotes": false, + "properties": true, + "page-preview": true, + "daily-notes": true, + "templates": true, + "note-composer": true, + "command-palette": true, + "slash-command": false, + "editor-status": true, + "bookmarks": true, + "markdown-importer": false, + "zk-prefixer": false, + "random-note": false, + "outline": true, + "word-count": true, + "slides": false, + "audio-recorder": false, + "workspaces": false, + "file-recovery": true, + "publish": false, + "sync": true, + "bases": true, + "webviewer": false +} \ No newline at end of file diff --git a/chrono.tasks/.obsidian/workspace.json b/chrono.tasks/.obsidian/workspace.json new file mode 100644 index 0000000..56d9d32 --- /dev/null +++ b/chrono.tasks/.obsidian/workspace.json @@ -0,0 +1,190 @@ +{ + "main": { + "id": "56a668de34c26d11", + "type": "split", + "children": [ + { + "id": "07a0aa696acfaf87", + "type": "tabs", + "children": [ + { + "id": "c7bccd0272521a1f", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "Docker.md", + "mode": "source", + "source": false + }, + "icon": "lucide-file", + "title": "Docker" + } + } + ] + } + ], + "direction": "vertical" + }, + "left": { + "id": "c74cb397c5dc27bb", + "type": "split", + "children": [ + { + "id": "973f93e99f586721", + "type": "tabs", + "children": [ + { + "id": "dad8458b7399dc0a", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical", + "autoReveal": false + }, + "icon": "lucide-folder-closed", + "title": "Files" + } + }, + { + "id": "e5d5e7559ed306af", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "", + "matchingCase": false, + "explainSearch": false, + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical" + }, + "icon": "lucide-search", + "title": "Search" + } + }, + { + "id": "aed67689a5311182", + "type": "leaf", + "state": { + "type": "bookmarks", + "state": {}, + "icon": "lucide-bookmark", + "title": "Bookmarks" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300 + }, + "right": { + "id": "4cc6aabbe0f085e2", + "type": "split", + "children": [ + { + "id": "d6f7737d064d74af", + "type": "tabs", + "children": [ + { + "id": "d0dca54f7dc291e1", + "type": "leaf", + "state": { + "type": "backlink", + "state": { + "file": "Docker.md", + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical", + "showSearch": false, + "searchQuery": "", + "backlinkCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-coming-in", + "title": "Backlinks for Docker" + } + }, + { + "id": "b1ca8745b20e876a", + "type": "leaf", + "state": { + "type": "outgoing-link", + "state": { + "file": "Docker.md", + "linksCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-going-out", + "title": "Outgoing links from Docker" + } + }, + { + "id": "d80519389a50945c", + "type": "leaf", + "state": { + "type": "tag", + "state": { + "sortOrder": "frequency", + "useHierarchy": true, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-tags", + "title": "Tags" + } + }, + { + "id": "a3d721b40278455a", + "type": "leaf", + "state": { + "type": "all-properties", + "state": { + "sortOrder": "frequency", + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-archive", + "title": "All properties" + } + }, + { + "id": "0d722d47271821e0", + "type": "leaf", + "state": { + "type": "outline", + "state": { + "file": "Docker.md", + "followCursor": false, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-list", + "title": "Outline of Docker" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300, + "collapsed": true + }, + "left-ribbon": { + "hiddenItems": { + "switcher:Open quick switcher": false, + "graph:Open graph view": false, + "canvas:Create new canvas": false, + "daily-notes:Open today's daily note": false, + "templates:Insert template": false, + "command-palette:Open command palette": false, + "bases:Create new base": false + } + }, + "active": "c7bccd0272521a1f", + "lastOpenFiles": [ + "Docker.md" + ] +} \ No newline at end of file diff --git a/chrono.tasks/Docker.md b/chrono.tasks/Docker.md new file mode 100644 index 0000000..eb93f9f --- /dev/null +++ b/chrono.tasks/Docker.md @@ -0,0 +1,47 @@ +# Chrono CCG - Project Guide + +This project is a hosted Blazor WebAssembly application with a PostgreSQL database for persisting agent notes. + +## Prerequisites +- **Docker Desktop**: Required for the recommended containerized setup. +- **.NET 10 SDK**: Required if you want to build or run the project locally without Docker. + +## 1. Running with Docker (Recommended) +The easiest way to get everything running (App + PostgreSQL) is using Docker Compose. + +1. **Open a terminal** in the project root (`Chrono/`). +2. **Run the following command**: + ```bash + docker-compose up --build + ``` +3. **Access the Application**: + - Web Interface: http://localhost:8080 + - API Endpoint: http://localhost:8080/api/notes + +The database will be automatically initialized and migrations will be applied on startup. + +## 2. Running Locally (Development) +If you need to run the app directly (e.g., for faster debugging): + +1. **Start a PostgreSQL database**. You can use the one from docker-compose if you want: + ```bash + docker-compose up db + ``` +2. **Verify Connection String**: `Server/appsettings.Development.json` is pre-configured to point to `localhost`. +3. **Run the Server project**: + ```bash + cd Server + dotnet run --launch-profile https + ``` +4. The app will be served at the URL shown in the terminal (e.g., https://localhost:7266). + +## 3. Running Tests +To verify the core domain logic: +```bash +dotnet test +``` + +## 4. Key Features +- **Agent Notes**: In the "Cards" gallery, select an Agent to see the "Personal Note" field. Changes are auto-saved to the PostgreSQL database when you click away from the text area. +- **Auto-Migrations**: The Server project automatically handles database schema updates on startup. +- **Dockerized Architecture**: Complete orchestration of the web server and database.