...docker test

This commit is contained in:
2026-06-18 18:35:56 -04:00
parent 5e1fe81473
commit 6a2a8abb22
31 changed files with 958 additions and 11 deletions
+3
View File
@@ -254,6 +254,9 @@ paket-files/
.idea/
*.sln.iml
# Docker environment variables
.env
# CodeRush
.cr/
+9
View File
@@ -0,0 +1,9 @@
**/.git
**/.obj
**/.bin
**/.vs
**/.vscode
**/.idea
bin/
obj/
publish/
+9
View File
@@ -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
+61
View File
@@ -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
+37
View File
@@ -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"]
+7
View File
@@ -0,0 +1,7 @@
namespace Chrono.Model;
public class CardNote
{
public string CardName { get; set; } = "";
public string Note { get; set; } = "";
}
+18
View File
@@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore;
using Chrono.Model;
namespace Server;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<CardNote> CardNotes { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CardNote>().HasKey(n => n.CardName);
}
}
@@ -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<ActionResult<CardNote>> 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<ActionResult<CardNote>> 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);
}
}
@@ -0,0 +1,43 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<string>("CardName")
.HasColumnType("text");
b.Property<string>("Note")
.IsRequired()
.HasColumnType("text");
b.HasKey("CardName");
b.ToTable("CardNotes");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Server.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CardNotes",
columns: table => new
{
CardName = table.Column<string>(type: "text", nullable: false),
Note = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CardNotes", x => x.CardName);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CardNotes");
}
}
}
@@ -0,0 +1,40 @@
// <auto-generated />
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<string>("CardName")
.HasColumnType("text");
b.Property<string>("Note")
.IsRequired()
.HasColumnType("text");
b.HasKey("CardName");
b.ToTable("CardNotes");
});
#pragma warning restore 612, 618
}
}
}
+43
View File
@@ -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<AppDbContext>(options =>
options.UseNpgsql(connectionString));
var app = builder.Build();
// Apply migrations
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
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();
@@ -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"
}
}
}
}
+27
View File
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\Model\Model.csproj" />
<ProjectReference Include="..\Web\Web.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
+6
View File
@@ -0,0 +1,6 @@
@Server_HostAddress = http://localhost:5256
GET {{Server_HostAddress}}/weatherforecast/
Accept: application/json
###
@@ -0,0 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=chrono;Username=postgres;Password=postgres"
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
+51
View File
@@ -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);
}
}
+31
View File
@@ -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();
}
}
+49 -1
View File
@@ -1,4 +1,5 @@
@page "/cards"
@inject HttpClient Http
<PageTitle>Card Gallery</PageTitle>
@@ -211,6 +212,19 @@
<span class="field-value">@selectedCard.ImmortalizeFrom</span>
</div>
}
@if (selectedCard.IsAgent)
{
<div class="detail-field note">
<span class="field-label"><i class="bi bi-pencil-fill"></i> Personal Note</span>
<textarea class="form-control note-input" @bind="currentNote" @onblur="SaveNote"
placeholder="Add a private note about this agent..."></textarea>
@if (isSaving)
{
<span class="saving-indicator">Saving...</span>
}
</div>
}
</div>
</div>
</div>
@@ -225,6 +239,8 @@
private string costFilter = "";
private CardData? selectedCard;
private List<string> 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<CardNote>($"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()
+2 -2
View File
@@ -4,13 +4,13 @@
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
<OverrideHtmlAssetPlaceholders>false</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all"/>
<PackageReference Include="Telerik.UI.for.Blazor" Version="14.0.0"/>
<PackageReference Include="Telerik.UI.for.Blazor" Version="14.0.0" PrivateAssets="none"/>
</ItemGroup>
<ItemGroup>
+23
View File
@@ -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;
}
+7 -8
View File
@@ -10,13 +10,12 @@
<link crossorigin href="https://fonts.gstatic.com" rel="preconnect"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet"/>
<link id="webassembly" rel="preload"/>
<link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"/>
<link href="css/app.css" rel="stylesheet"/>
<link href="_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css" rel="stylesheet"/>
<script src="_content/Telerik.UI.for.Blazor/js/telerik-blazor.js" defer></script>
<link href="favicon.png" rel="icon" type="image/png"/>
<link href="Web.styles.css" rel="stylesheet"/>
<link href="/lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"/>
<link href="/_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css" rel="stylesheet"/>
<link href="/css/app.css" rel="stylesheet"/>
<link href="/Web.styles.css" rel="stylesheet"/>
<script src="/_content/Telerik.UI.for.Blazor/js/telerik-blazor.js" defer></script>
<link href="/favicon.png" rel="icon" type="image/png"/>
<script type="importmap"></script>
</head>
@@ -46,7 +45,7 @@
<a class="reload" href=".">Reload</a>
<span class="dismiss">🗙</span>
</div>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
<script src="/_framework/blazor.webassembly.js"></script>
</body>
</html>
+47
View File
@@ -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.
+36
View File
@@ -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:
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="TelerikServer" value="https://nuget.telerik.com/v3/index.json" protocolVersion="3" />
</packageSources>
<packageSourceCredentials>
<TelerikServer>
<add key="Username" value="%TELERIK_USERNAME%" />
<add key="ClearTextPassword" value="%TELERIK_PASSWORD%" />
</TelerikServer>
</packageSourceCredentials>
</configuration>
+1
View File
@@ -0,0 +1 @@
{}
+3
View File
@@ -0,0 +1,3 @@
{
"theme": "obsidian"
}
+33
View File
@@ -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
}
+190
View File
@@ -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"
]
}
+47
View File
@@ -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.