From 7574476d7f30f46345dcdcf4aa1f5ab87e3022ea Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 31 Jul 2026 18:50:06 -0400 Subject: [PATCH] feat: add Docker deployment for CutList.Web and update CLAUDE.md docs Adds a Dockerfile and Gitea Actions workflow to build/push the CutList.Web image to the registry on push to master, enabling containerized deployment to forge. Also updates CLAUDE.md to reflect the CutList.Mcp project, REST API surface, and CatalogService, and tweaks the cut-length badge styling in the job editor. Co-Authored-By: Claude Sonnet 5 --- .gitea/workflows/build-cutlist.yml | 28 ++++++++++ .gitignore | 7 ++- CLAUDE.md | 54 +++++++++++++++++--- CutList.Web/Components/Pages/Jobs/Edit.razor | 2 +- CutList.Web/Dockerfile | 27 ++++++++++ 5 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 .gitea/workflows/build-cutlist.yml create mode 100644 CutList.Web/Dockerfile diff --git a/.gitea/workflows/build-cutlist.yml b/.gitea/workflows/build-cutlist.yml new file mode 100644 index 0000000..61a235e --- /dev/null +++ b/.gitea/workflows/build-cutlist.yml @@ -0,0 +1,28 @@ +name: Build CutList image + +on: + push: + branches: [master] + paths: + - "CutList.Web/**" + - "CutList.Core/**" + - "CutList.Web/Dockerfile" + - ".gitea/workflows/build-cutlist.yml" + workflow_dispatch: {} + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Log in to Gitea container registry + run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.thecozycat.net -u "${{ gitea.actor }}" --password-stdin + + - name: Build image + run: docker build -f CutList.Web/Dockerfile -t git.thecozycat.net/${{ gitea.repository_owner }}/cutlist:latest -t git.thecozycat.net/${{ gitea.repository_owner }}/cutlist:${{ gitea.sha }} . + + - name: Push image + run: | + docker push git.thecozycat.net/${{ gitea.repository_owner }}/cutlist:latest + docker push git.thecozycat.net/${{ gitea.repository_owner }}/cutlist:${{ gitea.sha }} diff --git a/.gitignore b/.gitignore index 3a2238d..74a780c 100644 --- a/.gitignore +++ b/.gitignore @@ -242,4 +242,9 @@ ModelManifest.xml .paket/paket.exe # FAKE - F# Make -.fake/ \ No newline at end of file +.fake/ + +# AlroCatalog scraper output +scripts/AlroCatalog/__pycache__/ +scripts/AlroCatalog/pdfs/ +scripts/AlroCatalog/screenshots/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index b2795ae..88f20ac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,15 +8,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co CutList is a 1D bin packing optimization application that helps users optimize material cutting. It calculates efficient bin packing solutions to minimize waste when cutting stock materials into required parts. -The solution contains three projects: +The solution contains four projects: | Project | Framework | Purpose | |---------|-----------|---------| | **CutList** | .NET 8.0 Windows Forms | Original desktop UI (MVP pattern) | | **CutList.Core** | .NET 8.0 Class Library | Domain models and packing algorithms (platform-agnostic) | -| **CutList.Web** | .NET 8.0 Blazor Server | Web-based UI with EF Core + SQL Server | +| **CutList.Web** | .NET 8.0 Blazor Server | Web-based UI + REST API, EF Core + SQL Server | +| **CutList.Mcp** | .NET 10.0 Console (stdio) | MCP server exposing CutList.Web's REST API as tools for Claude | -**Key Dependencies**: Math-Expression-Evaluator (input parsing), Newtonsoft.Json (serialization), Entity Framework Core (data access), Bootstrap 5 + Bootstrap Icons (UI) +**Key Dependencies**: Math-Expression-Evaluator (input parsing), Newtonsoft.Json (serialization), Entity Framework Core (data access), Bootstrap 5 + Bootstrap Icons (UI), ModelContextProtocol SDK (CutList.Mcp) ## Build Commands @@ -28,10 +29,11 @@ dotnet build CutList.sln dotnet build CutList/CutList.csproj dotnet build CutList.Core/CutList.Core.csproj dotnet build CutList.Web/CutList.Web.csproj +dotnet build CutList.Mcp/CutList.Mcp.csproj # Run applications dotnet run --project CutList/CutList.csproj # WinForms -dotnet run --project CutList.Web/CutList.Web.csproj # Blazor +dotnet run --project CutList.Web/CutList.Web.csproj # Blazor + REST API (default http://localhost:5270) # EF Core migrations (always apply immediately after creating) dotnet ef migrations add --project CutList.Web @@ -41,6 +43,18 @@ dotnet ef database update --project CutList.Web dotnet clean CutList.sln ``` +### Deploying CutList.Web as a Windows Service + +```powershell +powershell -ExecutionPolicy Bypass -File scripts/Deploy-CutListWeb.ps1 -ServiceName CutListWeb -InstallDir C:\Services\CutListWeb -Urls "http://*:5270" -OpenFirewall +``` + +Publishes, (re)creates the `CutListWeb` Windows service with auto-restart recovery, and optionally opens the firewall port. See `docs/deploy-script-guide.md` (global docs) for the template this follows. + +### Publishing CutList.Mcp + +CutList.Mcp is an stdio MCP server, not a hosted service — it's published to `~/.claude/mcp/CutList.Mcp/` and registered in `~/.claude/settings.local.json` (see global `CLAUDE.md` MCP Server Publishing table). It talks to CutList.Web's REST API at `http://localhost:5270`, so CutList.Web must be running (dev `dotnet run` or the deployed Windows service) for the MCP tools to work. + ## Architecture ### CutList.Core — Domain & Algorithms @@ -73,11 +87,25 @@ dotnet clean CutList.sln - `CutListService` bridges UI models to core packing algorithms - `DocumentService` handles JSON file persistence -### CutList.Web (Blazor Server) — Web UI +### CutList.Web (Blazor Server) — Web UI + REST API **Database**: SQL Server via Entity Framework Core (connection string: `DefaultConnection`) -**Service Registration** (Program.cs): All services registered as Scoped — MaterialService, SupplierService, StockItemService, JobService, CutListPackingService, ReportService, PurchaseItemService +**Service Registration** (Program.cs): All services registered as Scoped — MaterialService, SupplierService, StockItemService, JobService, CutListPackingService, ReportService, PurchaseItemService, CatalogService. `IDbContextFactory` is used (not a scoped `DbContext` directly) for Blazor Server circuit safety. + +**REST API** (`Controllers/`): `JobsController`, `MaterialsController`, `StockItemsController`, `SuppliersController`, `CuttingToolsController`, `PackingController`, `CatalogController` — Swagger/OpenAPI enabled in Development. This API is the integration surface `CutList.Mcp` calls into; the Blazor UI talks to the services directly and does not go through it. + +**Error handling**: `UseExceptionHandler("/Error", ...)` in non-Development environments routes to `Components/Pages/Error.razor`. + +### CutList.Mcp — MCP Server + +Stdio-transport MCP server (`ModelContextProtocol` SDK) exposing CutList.Web's REST API as tools for Claude Code. Registers tools via `WithToolsFromAssembly`; logging is disabled entirely so it doesn't interfere with the stdio transport. + +- `ApiClient.cs` — typed `HttpClient` wrapper for CutList.Web's REST API (`BaseAddress` hardcoded to `http://localhost:5270`) +- `JobTools.cs` — job CRUD, parts/stock, optimization (`OptimizeJob`), cutting tools +- `InventoryTools.cs` — suppliers, materials, stock items, supplier offerings +- `CutListTools.cs` — static helpers shared across tool classes +- `Models.cs` — shared DTOs distinct from CutList.Web's own DTOs (kept intentionally thin for MCP tool responses) ## CutList.Web Entities @@ -166,6 +194,10 @@ Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its ### ReportService - `FormatLength(inches)`, `GroupItems(items)` for print report formatting +### CatalogService +- `ExportAsync()` — dumps active suppliers, cutting tools, and materials (with dimensions + stock items + supplier offerings) into a shape-grouped `CatalogData` DTO for bulk export/import tooling +- Backs the `CatalogController` REST endpoint and the `scripts/ExportData` / `scripts/AlroCatalog` data-loading workflows + ## CutList.Web Pages | Route | Page | Purpose | @@ -183,6 +215,7 @@ Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its | `/suppliers` | Suppliers/Index | Supplier list with CRUD | | `/suppliers/{Id}` | Suppliers/Edit | Supplier + offerings management | | `/tools` | Tools/Index | Cutting tools CRUD | +| `/Error` | Error | Unhandled exception page (registered via `UseExceptionHandler`) | ## Shared Components @@ -208,6 +241,13 @@ Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its - **Timestamps** — `CreatedAt` defaults to `GETUTCDATE()`; `UpdatedAt` set on modifications - **Collections** — Encapsulated in Core; use `AsReadOnly()`, access via `Add*` methods - **Priority system** — Lower priority bins used first in packing algorithm +- **UI ↔ MCP split** — The Blazor UI calls services directly (in-process); CutList.Mcp and any other external integration go through the REST API in `Controllers/`. Keep both paths in sync when changing service method signatures used by controllers. + +## Supporting Scripts (`scripts/`) + +- `Deploy-CutListWeb.ps1` — publishes and installs CutList.Web as a Windows Service (see Build Commands above) +- `ExportData/` — standalone console project that exercises `CutList.Web`'s data layer to import/export catalog seed data (e.g. `Data/SeedData/oneals-catalog.json`) +- `AlroCatalog/` — Python scraper (`scrape_alro.py`) for pulling material/size/grade data from the Alro Steel SmartGrid site into a JSON catalog for import; see `SCRAPE_PLAN.md` for scraper status and cascading-dropdown navigation notes. Not part of the .NET build. ## Key Files @@ -221,4 +261,6 @@ Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its | `CutList.Web/Services/JobService.cs` | Job orchestration (CRUD, parts, stock, tools, lock/unlock) | | `CutList.Web/Services/CutListPackingService.cs` | Bridges web entities to Core packing engine | | `CutList.Web/Components/Pages/Jobs/Edit.razor` | Job editor (tabbed: Details, Parts, Stock, Results) | +| `CutList.Mcp/ApiClient.cs` | HTTP client the MCP server uses to call CutList.Web's REST API | +| `CutList.Mcp/Program.cs` | MCP server entrypoint (stdio transport, tool registration) | | `CutList/Presenters/MainFormPresenter.cs` | WinForms business logic orchestrator | diff --git a/CutList.Web/Components/Pages/Jobs/Edit.razor b/CutList.Web/Components/Pages/Jobs/Edit.razor index 4dfeb3f..25704bb 100644 --- a/CutList.Web/Components/Pages/Jobs/Edit.razor +++ b/CutList.Web/Components/Pages/Jobs/Edit.razor @@ -1172,7 +1172,7 @@ else @foreach (var item in entry.Bin.Items) { - + @(string.IsNullOrWhiteSpace(item.Name) ? ArchUnits.FormatFromInches(item.Length) : $"{item.Name} ({ArchUnits.FormatFromInches(item.Length)})") } diff --git a/CutList.Web/Dockerfile b/CutList.Web/Dockerfile new file mode 100644 index 0000000..1162ae1 --- /dev/null +++ b/CutList.Web/Dockerfile @@ -0,0 +1,27 @@ +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src + +# Copy solution and project files for restore +COPY CutList.sln . +COPY CutList.Web/CutList.Web.csproj CutList.Web/ +COPY CutList.Core/CutList.Core.csproj CutList.Core/ +RUN dotnet restore CutList.Web/CutList.Web.csproj + +# Copy everything else and build +COPY . . +WORKDIR /src/CutList.Web +RUN dotnet publish -c Release -o /app/publish + +# Runtime stage +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime +WORKDIR /app + +COPY --from=build /app/publish . + +EXPOSE 5270 + +ENV ASPNETCORE_URLS=http://+:5270 +ENV ASPNETCORE_ENVIRONMENT=Production + +ENTRYPOINT ["dotnet", "CutList.Web.dll"]