The shift-in-place approach broke when SortOrder values had gaps from soft
deletes. Switch to remove/insert then assign SortOrder = index, which is
correct regardless of initial values. Also fix OutgosController Reorder
which had AsNoTracking applied from the M-3 change, silently dropping saves.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses production CPU spike incident. Key changes:
- Guard OTel exporter behind OTEL_EXPORTER_OTLP_ENDPOINT env var; filter
tracing to /api paths only — unconditional export was primary suspect
- Remove /healthz endpoint entirely (unauthenticated, hit DB on every call)
- Replace KnownUserMiddleware with POST /api/users/me called once on login
from TokenSync — eliminates unconditional DB write on every request
- Add DB indexes: (BudgetId, IsDeleted) on Incomes/Outgos, OwnerUserId on
Budgets, SharedWithUserId and (IsPending, SharedWithEmail) on BudgetShares
- Move UseRateLimiter() before UseStaticFiles() so all requests are throttled
- Replace full-array reorder with move-by-position (id + newIndex) — bounded
input, fewer DB writes, better API design
- Lock ForwardedHeaders to 172.20.0.0/16 subnet; fixes KnownNetworks
deprecation warning (0 warnings in build now)
- Add AsNoTracking() to all read-only queries in Summary/Incomes/OutgosController
- FrequencyCalculator returns 0 for unknown enum values instead of throwing
- Thread.Sleep → await Task.Delay in OIDC startup loop
- AllowedHosts locked to budget.stwaddle.com
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three corrections vs Auth project:
- Replace AddOtlpExporter() (traces only) with UseOtlpExporter() so both
traces and logs are exported via OTLP
- Remove redundant .WithLogging() call; builder.Logging.AddOpenTelemetry()
is sufficient on its own
- Pass OTEL_EXPORTER_OTLP_ENDPOINT/PROTOCOL through from host env vars
instead of hardcoding the collector URL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds OTel packages (Extensions.Hosting, Instrumentation.AspNetCore/Http/EFCore,
Exporter.OTLP) and configures logging + tracing in Program.cs. Endpoint is
intentionally left unset in code — read from OTEL_EXPORTER_OTLP_ENDPOINT at runtime.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migration adds IsDeleted/DeletedAt to Budgets, Incomes, Outgos, BudgetShares
- xmin concurrency token configured as shadow property (uint, xid type) — not added
as a column in the migration since xmin is a PostgreSQL system column
- Budget.RowVersion property removed; concurrency tracked via shadow property only
- Add ISoftDeletable interface with IsDeleted/DeletedAt
- Implement on Budget, Income, Outgo, BudgetShare; add RowVersion (xmin) to Budget
- Configure EF global query filters and xmin concurrency token
- Replace Remove() with soft delete in all delete endpoints
- Wrap Budget.Update SaveChanges in DbUpdateConcurrencyException catch
- Install @tanstack/react-query
- Create queryClient with MutationCache for global toast on success/error
- QueryToastBridge component bridges module-level handlers to ToastProvider
- Wrap App in QueryClientProvider
- Create domain hook files: budgets, incomes, outgos, shares, summary
- Update all 5 pages to use hooks; remove inline useEffect/useState fetch logic
- DnD reorder pages use local displayItems state synced from query data
Deletes the hand-rolled AuthContext/UserManager setup and replaces it
with AuthProvider from react-oidc-context. onSigninCallback clears the
OIDC code params from the URL (unless an error is present). TokenSync
bridges the library token into the existing api/client setTokenProvider
pattern. AuthGuard updated to use auth.isLoading/isAuthenticated/
signinRedirect from the library. CallbackPage simplified to a passive
error renderer — react-oidc-context processes the OIDC exchange itself.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Budget.Core: entities, DTOs, enums, FrequencyCalculator (no EF/ASP.NET deps)
Budget.Infrastructure: AppDbContext, migrations, BudgetAuthorizationService
Budget.Api: controllers, middleware, Program.cs — references both projects
EF and Npgsql packages moved to Infrastructure; Api retains only JwtBearer,
HealthChecks, and EF.Design (needed for dotnet ef CLI). Dockerfile updated
to copy all three project directories before publishing. Migration namespaces
updated from Budget.Api.Data.* to Budget.Infrastructure.Data.* and model
type strings updated to Budget.Core.Models.* in the snapshot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renames VITE_AUTH_* to VITE_OIDC_* to match the stack convention.
Adds a dedicated VITE_OIDC_POST_LOGOUT_REDIRECT_URI instead of deriving
it from the redirect URI via string replace. Switches from Dockerfile
ARG/ENV to a committed src/Budget.Client/.env so Vite picks up
production values at build time without needing build-arg overrides.
.env.local is gitignored for localhost dev overrides.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Catches all unhandled exceptions, logs them, and returns
{ "error": "An unexpected error occurred." } with HTTP 500.
Registered before all other middleware so nothing leaks through.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both policies partition by sub claim with IP fallback. Global limiter
applies to all requests; writes policy is applied via
[EnableRateLimiting("writes")] on every POST, PUT, and DELETE action
across all five controllers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clears KnownNetworks/KnownProxies to trust X-Forwarded-For from any
upstream, since nginx-proxy sits at a dynamically assigned internal IP.
Without this, RemoteIpAddress is always the proxy IP, breaking any
per-client IP resolution.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Blocks startup for up to 2 minutes retrying the OIDC discovery doc fetch,
then proceeds anyway. Prevents JWT middleware from failing to initialize
when the auth and app containers start simultaneously.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Authority, Audience, and MetadataAddress are not secrets so they belong
in committed config rather than runtime env vars. MetadataAddress points
to the internal Docker URL for JWKS fetch, avoiding nginx hairpinning;
it is blanked in Development so the JWT middleware falls back to
Authority-based discovery. RequireHttpsMetadata is disabled only when
MetadataAddress is set (internal http URL).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove OwnerUserId from BudgetDto: OIDC sub of the budget owner was
being returned to all collaborators (including View-only users)
- Remove SharedWithUserId from ShareDto: other users' internal OIDC subs
were visible to anyone with read access to a budget
- Delete MeController: scaffolding endpoint that returned sub to the
browser; no legitimate frontend use case
- Restrict /healthz to require authorization: prevents unauthenticated
probing of database connectivity
- Add input validation annotations to all request DTOs: [Required],
[MaxLength], [Range(0,0.9999)] on EffectiveTaxRate, [EmailAddress] on
share email — [ApiController] now returns 400 instead of 500 for
invalid input hitting DB constraints
- Replace User.FindFirst("sub")!.Value with GetUserId() extension across
all controllers: returns 401 instead of NullReferenceException (500)
if a token lacks a sub claim
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ErrorBoundary component wrapping the whole app
- Add ToastProvider with showError/showInfo; Income and Outgo pages use it for API errors
- Add LoadingSkeleton component with shimmer animation; Income and Outgo show it while loading
- Add confirm-on-delete dialogs for income and outgo rows
- Apply EF migrations automatically on startup via MigrateAsync()
- Add /healthz health check endpoint using DbContext check
- Add Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore package
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add SummaryController: computes monthly income, breakdown by type (Need/Want/Save/Unspent), and pre-tax income
- Need/Want/Save get target% (50/30/20), maxAmount, and remaining; Unspent shows totals only
- PUT /summary/tax-rate updates EffectiveTaxRate on the budget (no new migration needed)
- Add SummaryDto, SummaryBreakdownItem, PreTaxIncomeDto DTOs
- Add Summary page: income header cards, type breakdown table with ⓘ tooltip for target%,
pre-tax section with editable tax rate field
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add JWT Bearer auth to ASP.NET (authority/audience from AUTH__AUTHORITY / AUTH__AUDIENCE config)
- Add KnownUserMiddleware: upserts KnownUser and resolves pending shares on each authenticated request
- Add MeController as a guarded test endpoint (/api/me)
- Add oidc-client-ts + react-router-dom to client
- Create AuthContext/AuthProvider with login, logout, token storage
- Create AuthGuard component protecting all budget routes
- Add stub /callback page for OIDC redirect handling
- Wire up all routes in App.tsx with SPA routing structure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>