Updated plan to fix issues
This commit is contained in:
@@ -0,0 +1,74 @@
|
|||||||
|
# OTel Integration — Budget
|
||||||
|
|
||||||
|
Wire up OpenTelemetry in Budget.Api so logs and traces flow through the OTel Collector
|
||||||
|
to Seq. Depends on the infrastructure plan (`docker/.plans/otel-seq-infrastructure.md`)
|
||||||
|
being applied first.
|
||||||
|
|
||||||
|
## Packages to add (Budget.Api.csproj)
|
||||||
|
|
||||||
|
Verify latest stable versions on NuGet before adding — do not guess.
|
||||||
|
|
||||||
|
```
|
||||||
|
OpenTelemetry.Extensions.Hosting
|
||||||
|
OpenTelemetry.Instrumentation.AspNetCore
|
||||||
|
OpenTelemetry.Instrumentation.Http
|
||||||
|
OpenTelemetry.Instrumentation.EntityFrameworkCore
|
||||||
|
OpenTelemetry.Exporter.OpenTelemetryProtocol
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 1 — Wire up OTel in Program.cs
|
||||||
|
|
||||||
|
Add after `var builder = WebApplication.CreateBuilder(args);`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Logging.AddOpenTelemetry(logging =>
|
||||||
|
{
|
||||||
|
logging.IncludeFormattedMessage = true;
|
||||||
|
logging.IncludeScopes = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddOpenTelemetry()
|
||||||
|
.ConfigureResource(resource => resource
|
||||||
|
.AddService(serviceName: "budget", serviceVersion: "1.0.0"))
|
||||||
|
.WithLogging()
|
||||||
|
.WithTracing(tracing => tracing
|
||||||
|
.AddAspNetCoreInstrumentation()
|
||||||
|
.AddHttpClientInstrumentation()
|
||||||
|
.AddEntityFrameworkCoreInstrumentation()
|
||||||
|
.AddOtlpExporter());
|
||||||
|
```
|
||||||
|
|
||||||
|
The `AddOtlpExporter()` call with no arguments reads the endpoint from environment variables
|
||||||
|
(`OTEL_EXPORTER_OTLP_ENDPOINT`), keeping config out of code.
|
||||||
|
|
||||||
|
## Phase 2 — docker-compose environment variables
|
||||||
|
|
||||||
|
Add to the `budget` service in `docker/docker-compose.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
||||||
|
- OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
||||||
|
- OTEL_SERVICE_NAME=budget
|
||||||
|
```
|
||||||
|
|
||||||
|
`otel-collector` resolves via the `telemetry` network added in the infrastructure plan.
|
||||||
|
|
||||||
|
## Phase 3 — Verification
|
||||||
|
|
||||||
|
1. Build and push a new Budget image
|
||||||
|
2. `docker compose up -d budget`
|
||||||
|
3. Make a few API calls (create a transaction, fetch accounts)
|
||||||
|
4. In Seq, search `@ServiceName = 'budget'` — you should see structured log events
|
||||||
|
including the OIDC discovery retry loop logs already present in Program.cs
|
||||||
|
5. Confirm DB spans appear for EF Core queries
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Budget already has structured logging calls in the OIDC startup retry loop
|
||||||
|
(`startupLogger.LogInformation`, `LogWarning`). These will automatically become
|
||||||
|
structured events in Seq with no code changes.
|
||||||
|
- The `ErrorHandlingMiddleware` and `KnownUserMiddleware` are good candidates to add
|
||||||
|
structured properties (e.g. `userId`, `endpoint`) to log calls, making Seq searches
|
||||||
|
more useful.
|
||||||
|
- `AddHttpClientInstrumentation()` will trace outgoing calls to the Auth server (OIDC
|
||||||
|
discovery, token introspection), which is useful for diagnosing auth latency.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Backend (ASP.NET Core)
|
||||||
|
```bash
|
||||||
|
# Run API (must listen on port 5000 for the Vite proxy to work)
|
||||||
|
cd src/Budget.Api
|
||||||
|
ASPNETCORE_URLS=http://localhost:5000 dotnet run
|
||||||
|
|
||||||
|
# Build
|
||||||
|
dotnet build src/Budget.Api/Budget.Api.csproj
|
||||||
|
|
||||||
|
# Add an EF Core migration
|
||||||
|
dotnet ef migrations add <Name> --project src/Budget.Infrastructure --startup-project src/Budget.Api
|
||||||
|
|
||||||
|
# Remove last migration (before it has been applied)
|
||||||
|
dotnet ef migrations remove --project src/Budget.Infrastructure --startup-project src/Budget.Api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (Vite/React)
|
||||||
|
```bash
|
||||||
|
cd src/Budget.Client
|
||||||
|
npm run dev # dev server at http://localhost:5173
|
||||||
|
npm run build # tsc + vite build (what CI and Docker use)
|
||||||
|
npm run lint # eslint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
```powershell
|
||||||
|
.\build-budget-image.ps1 # builds multi-stage image and pushes to docker.stwaddle.com/budget:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The app is a single Docker container: the .NET API serves the compiled React SPA as static files and also handles all `/api` requests. In development the two processes run separately; Vite proxies `/api` to `http://localhost:5000`.
|
||||||
|
|
||||||
|
### Project layout
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
Budget.Core/ # Domain models, DTOs, FrequencyCalculator — no EF, no ASP.NET
|
||||||
|
Budget.Infrastructure/# EF Core DbContext, migrations, BudgetAuthorizationService
|
||||||
|
Budget.Api/ # ASP.NET Core controllers, middleware, Program.cs
|
||||||
|
Budget.Client/ # Vite/React SPA
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data model
|
||||||
|
- **Budget** (owner, name) → owns **Incomes**, **Outgos**, **BudgetShares**
|
||||||
|
- All entities use **soft delete** (`IsDeleted` / `DeletedAt`) with a global EF query filter — hard deletes are not used
|
||||||
|
- **Budget** uses PostgreSQL's `xmin` shadow property as an EF concurrency token
|
||||||
|
- **BudgetShare** links a budget to another user by email; `IsPending = true` until the target user logs in (resolved by `KnownUserMiddleware`)
|
||||||
|
- **Frequency** is a C# enum (`Monthly`, `Biweekly`, `Annually`, etc.); `FrequencyCalculator` converts any frequency to monthly/annual equivalents
|
||||||
|
- The 50/30/20 rule is hardcoded in `SummaryController`: Need=50%, Want=30%, Save=20%
|
||||||
|
|
||||||
|
### API conventions
|
||||||
|
- All controllers require `[Authorize(Roles = "admin,user")]` — tokens that lack a `role` claim matching either value get a 403
|
||||||
|
- `RoleClaimType = "role"` and `NameClaimType = "sub"` are set in JWT options; role checks work with `User.IsInRole()`
|
||||||
|
- `BudgetAuthorizationService` centralises read/write/owner checks against the budget's owner and any accepted shares — always call `CanReadAsync` / `CanWriteAsync` / `IsOwnerAsync` before touching budget data
|
||||||
|
- The `TryGetUserId` helper pattern (returns an `IActionResult?` error or out-params the user id) is used consistently across all controllers
|
||||||
|
- **Enum serialisation**: `JsonStringEnumConverter` is registered globally, so all C# enums are sent/received as strings (e.g. `"Monthly"`, not `5`)
|
||||||
|
- Write endpoints use the `"writes"` rate-limit policy (30 req/min per user); reads use the global limiter (120 req/min)
|
||||||
|
- EF migrations run automatically via `MigrateAsync()` at startup
|
||||||
|
|
||||||
|
### Frontend conventions
|
||||||
|
- **All server state** goes through TanStack Query (`src/api/`). Query keys follow the pattern `['budgets']`, `['budgets', id]`, `['budgets', id, 'summary']`, etc.
|
||||||
|
- **All forms** use `react-hook-form` + `zod` schemas defined in `src/schemas/index.ts`. Never use uncontrolled inputs outside of RHF.
|
||||||
|
- **CSS design system** lives entirely in `src/index.css` as CSS custom properties — no Tailwind, no CSS modules. Use the existing variables (`--color-primary`, `--green-*`, etc.) rather than hardcoded hex values. `App.css` is intentionally near-empty.
|
||||||
|
- **Icons** come from `lucide-react` (tree-shaken per-import). Use `btn-icon` class for icon-only buttons; always add a `title` attribute for accessibility.
|
||||||
|
- **Inline table editing**: rows show read-only data; clicking a cell enters edit mode in-place using a `useState(editing)` toggle. New rows use a separate input row appended to `<tbody>` (not a modal).
|
||||||
|
- **Sorting**: `SortableHeader` component and `SortState` type in `src/components/SortableHeader.tsx`. Sorting is client-side only and derives a view from the drag-ordered `displayItems` array — clearing sort restores drag order. Drag handles are disabled while a sort is active.
|
||||||
|
- **Frequency calculations** for real-time previews are in `src/utils/frequency.ts`, mirroring the C# `FrequencyCalculator` exactly.
|
||||||
|
- **Auth**: `react-oidc-context` wraps the whole app; `AuthGuard` protects all budget routes; `TokenSync` wires the OIDC access token into the API client on every auth state change. After OIDC callback, navigation uses `window.location.replace('/')` (not `history.replaceState`) so React Router picks up the URL change.
|
||||||
|
|
||||||
|
### Auth / OIDC
|
||||||
|
- Authority: `https://auth.stwaddle.com/` (OpenIddict-based, at `src.Auth.Server` in a sibling repo)
|
||||||
|
- API audience claim: `budget-api` (must match `AUTH__AUDIENCE` env var)
|
||||||
|
- Scope: `budget_api` (underscore, not hyphen)
|
||||||
|
- Roles `admin` and `user` are granted per-user on the client registration in the auth server admin panel. A user with no role gets `access_denied` at the authorize endpoint.
|
||||||
|
- Frontend OIDC config is baked into the build via `VITE_*` env vars; override locally in `src/Budget.Client/.env.local`
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.\build-image.ps1
|
||||||
|
.\update-container.ps1
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
$image = "docker.stwaddle.com/budget:latest"
|
|
||||||
docker build -t $image .
|
|
||||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
||||||
docker push $image
|
|
||||||
ssh stwaddle_com "cd stwaddlecom; ./update-budget.sh"
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh stwaddle_com "cd stwaddlecom; docker compose pull budget; docker compose down budget; docker compose up -d budget"
|
||||||
Reference in New Issue
Block a user