Conversation
…apture default off, cache pattern, SHA256 Phase 1.5 audit-hotfix wave A (H1, H8, H17, H18): H1 — Unify JWT ClockSkew. Six wiring points (Auth JwtConfiguration, Auth DependencyInjection, Auth TokenService 2×, Messaging Program, Realtime Program) used TimeSpan.Zero; BuildingBlocks consumer extension used 30s; Gateway used 5s; SecurityConstants.TokenClockSkewSeconds was 5 and unused. Now every wiring reads SecurityConstants.SecurityPolicies.TokenClockSkewSeconds (set to 30 s — tight enough to bound post-expiry replay, loose enough to tolerate Fly NTP drift). Pinned tests updated. H8 — EF SetDbStatementForText defaults to false to remove the PII risk documented in TelemetryConfiguration's own XML doc. Opt in via OpenTelemetry:Tracing:CaptureDbStatementText=true per-environment. H17 — CacheService.RemoveByPatternAsync implemented via Redis SCAN + KeyDeleteAsync (UNLINK) in 500-key batches. Prefixes the StackExchange Redis instance-name to the pattern; skips replicas; cancellation-aware; no-ops cleanly when no IConnectionMultiplexer is registered. L1 layer remains TTL-bounded. H18 — Idempotent fallback GUID hash: MD5 → SHA256 truncated to 16 bytes. Removes the CA5351 static-analyzer flag with identical determinism. https://claude.ai/code/session_01B59DWTDTzpx4yRCLYZEhgQ
…mpose, npm-audit high, NuGet cache, CD liveness probe Phase 1.5 audit-hotfix wave B (H5, H7, H16, H21, H22, H23, P2-MIG-002): H5 — Pin superfly/flyctl-actions/setup-flyctl to commit ed8efb3 (v1.6) across all four CD workflow occurrences. Closes the supply-chain risk explicitly flagged by the existing TODO comment. H7 — docker-compose service healthchecks switched from aggregate /health to /health/ready, matching INV-OBS-4 semantics and the Fly manifest probes. depends_on.condition: service_healthy now means "ready to serve traffic", not "process is alive". H16 — npm-audit threshold raised from moderate to high. The frontend is public-facing; High-severity transitive CVEs should block CI, not pass silently. H21 — Trivy IaC scan now has two passes: the first uploads SARIF for the GitHub Security tab (informational), the second fails the job on any HIGH or CRITICAL finding. MEDIUM is intentionally informational — Trivy's MEDIUM rules are noisy at the IaC layer. H22 — actions/setup-dotnet@v5 cache: true enabled across ci.yml, security.yml, openapi.yml, migrations.yml. cache-dependency-path hashes every csproj, Directory.Packages.props, and Directory.Build.props so the key changes when (and only when) the restore graph changes. Expected restore-time reduction ~60-80% on warm cache. H23 — CD smoke now probes /health/live (15× × 2 s = 30 s window) BEFORE the /health/ready poll. Distinguishes "gateway process crashed" from "backends slow to warm up". A failed liveness fails fast instead of burning two minutes on readiness retries. P2-MIG-002 — Migration script idempotence check. The migrations workflow now greps for "IF [NOT] EXISTS" markers in the generated SQL and fails if a non-empty script lacks guards, catching any future EF tooling regression where --idempotent silently produces non-idempotent output. https://claude.ai/code/session_01B59DWTDTzpx4yRCLYZEhgQ
…SP, CSRF retry, traceparent reuse, cross-tab logout Phase 1.5 audit-hotfix wave C (H9, H10, H11, H13, H14, H15): H9 — Landing-page footer year guarded by the existing `mounted` flag. auth/login and auth/register already use the same pattern; brings the root marketing route in line. Removes the SSR/CSR DOM-text divergence on year rollover and clock-skew machines. H10 — Zustand onRehydrateStorage now explicitly sets isAuthenticated=false when accessToken is absent on rehydrate. accessToken is in-memory only so it's always absent post-rehydration; pinning the flag closes a brief render window where guards could see isAuthenticated=true before restoreSession() resolves. H11 — Main axios client now retries a state-changing 403 once with a fresh CSRF token (matching the existing auth-public.ts pattern). The retry flag _csrfRetry prevents infinite recursion: a second 403 on the same logical request propagates to the caller. Test rewritten to pin the retry branch and the no-retry branch. H13 — Cross-tab logout via BroadcastChannel. The Zustand store persists to sessionStorage (per-tab), so the native `storage` event won't fire cross-tab. clearAuth() now publishes a `logout` message on the `planora-auth` channel; the SecurityInitializer subscribes and calls clearAuth(true) on receipt (silent flag prevents an echo loop). A new @/lib/auth-broadcast module owns the channel name and the publisher. H14 — On a 401 retry, the original request's trace-id is preserved (traceparentForExistingTrace) while a fresh span-id is generated. Keeps backend trace correlation intact across the silent-refresh round-trip instead of producing two unconnected traces. H15 — CSP additions: object-src 'none', child-src 'none', worker-src 'self'. Defence-in-depth against reflected XSS payloads using <object>, <embed>, or worker spawn. style-src 'unsafe-inline' stays — documented trade-off for Tailwind/Next.js critical CSS injection. Tests: 360/360 green; lint clean (only pre-existing warnings in navbar); type-check clean. https://claude.ai/code/session_01B59DWTDTzpx4yRCLYZEhgQ
… wrapper removal, todo description, migrator drift, CODEOWNERS Phase 1.5 audit-hotfix wave D (H2, H3, H4, H6, H19): H2 — Refresh-token reuse detection. RefreshTokenCommandHandler now treats presentation of a previously-rotated token (revoked with reason "Replaced by new token") as a replay attack: every active refresh token on the user is revoked with reason "Reuse detected — chain invalidated", the security stamp is rotated (every minted access token gets rejected on next call), and an Unauthorized response is returned. Adds ISecurityStampService dependency to the handler; new xUnit theory pins the chain-invalidation behaviour. Closes the audit's P2-Backend "no refresh-token reuse detection" finding. H3 — Todo description validator now MaximumLength(2000) on Create and Update, matching TodoItemConfiguration.HasMaxLength(2000). Eliminates the silent server-side truncation gap between FluentValidation's old 5000 ceiling and the actual varchar(2000) column. Direction chosen so no existing data could exceed the new limit (column was always the ground truth). H4 — Auth API telemetry wrapper removed. Services/AuthApi/.../Configuration/ OpenTelemetryExtensions.cs is deleted; Program.cs now calls the canonical BuildingBlocks AddPlanoraTelemetry(configuration, "AuthService") directly, matching every other service and INV-OBS-5. Two test files migrated to assert on the canonical surface. H6 — Planora.Migrator now refuses to start a migration run when the database has applied migrations that are absent from the compiled code base (schema drift). Operators must reconcile (restore the missing migration files in code, or reset the target environment) before re- running. Removes the "delete-a-migration-locally-then-deploy" foot-gun called out in the DevOps audit P1-MIG-001. H19 — CODEOWNERS file. Codifies which surfaces (security primitives, observability pipeline, outbox state machine, migrator, CI/CD, deployment manifests, INVARIANTS) need reviewer attention. Branch protection's "require code owner review" toggle can now enforce it. https://claude.ai/code/session_01B59DWTDTzpx4yRCLYZEhgQ
…telemetry, schema-drift Phase 1.5 audit-hotfix wave E (documentation closure): INVARIANTS.md — five new closed-form rules: - INV-AUTH-6 (refresh-token reuse detection): rotated-token replay must invalidate the entire chain and rotate the security stamp. - INV-AUTH-7 (ClockSkew single source of truth): every JWT wiring reads SecurityConstants.SecurityPolicies.TokenClockSkewSeconds; literals are forbidden, pinned tests enforce equality. - INV-FLOW-5 (migrator schema-drift guard): applied set must be a subset of code set; migrator fails fast on drift. - INV-OBS-5 strengthened: forbid per-service wrappers around the canonical AddPlanoraTelemetry; EF SQL text capture is opt-in only. - INV-FLOW-4 amended: migrations workflow asserts idempotence markers in every non-empty generated SQL script. auth-security.md — new RefreshTokenCommandHandler (reuse path) row in the security-stamp rotation table. CHANGELOG.md — full Phase 1.5 hotfix-wave entry covering waves A-D (H1 through H6, H8-H11, H13-H19, H21-H23, plus P2-MIG-002). https://claude.ai/code/session_01B59DWTDTzpx4yRCLYZEhgQ
…s (H12, H20)
Phase 1.5 audit-hotfix wave F:
H12 — AbortController plumbed through the tasks page mount-time fetch
trio. fetchActiveTodos, fetchCompletedPreview, fetchCategories now accept
an optional AbortSignal; the page-level useEffect creates one controller,
runs the three fetches in parallel via Promise.all, and aborts on
cleanup. axios.isCancel + signal.aborted guard every catch and every
setState past an await. Pagination loop checks signal.aborted between
pages so a rapid route switch does not keep paginating a stale list.
Scope: tasks page only. Dashboard / categories / profile fetch chains
have more complex orchestrations (multiple effects per page) and will
follow in a dedicated commit; tasks was the explicit P1-DATA-FETCH-RACE
target in the audit.
H20 — Opt-in pre-commit hooks. Two cheap gates that match what CI already
enforces but run locally in the few seconds before a commit:
- ESLint with --max-warnings=0 on staged frontend files (requires
frontend/node_modules present — gracefully skips if not installed)
- dotnet format --verify-no-changes on Planora.sln if any .cs/.csproj
is staged (gracefully skips if the dotnet CLI is not on PATH)
Implementation chosen for the polyglot tree: a plain executable
.githooks/pre-commit shell script plus a one-shot installer at
scripts/install-hooks.sh that calls `git config core.hooksPath
.githooks`. No npm dependency on husky, no root package.json required,
no implicit install on `npm install`. Contributors run the installer
once per clone; `git commit --no-verify` bypasses for emergencies.
CONTRIBUTING.md documents the opt-in.
Frontend tests: 360/360 green; type-check clean; lint clean.
https://claude.ai/code/session_01B59DWTDTzpx4yRCLYZEhgQ
… into BuildingBlocks (Phase 2)
Replaces three drift surfaces (per-service BaseRepository in Auth and
Messaging; per-service OutboxRepository in Auth, Messaging, Category)
with two canonical implementations in
BuildingBlocks.Infrastructure.Persistence. Per-service classes survive
as [Obsolete] adapters for one release; they thin-wrap the canonical
implementation so behaviour is now identical regardless of which
import path a future caller picks.
BaseRepository<TEntity, TId, TContext> (BuildingBlocks):
- Single source of truth for soft-delete filtering, AsNoTracking on
read paths (INV-DATA-3 alignment — Todo/Messaging previously omitted
it), pagination, and specification dispatch.
- GetByIdAsync intentionally tracks (so callers can chain Update);
every other read uses AsNoTracking.
- Explicit !IsDeleted predicate plus services that also configure
HasQueryFilter (Auth) get defence-in-depth — the SQL optimiser
collapses the redundant predicate.
Auth.Infrastructure.Persistence.Repositories.BaseRepository<T>:
- Now a thin [Obsolete] adapter inheriting from the canonical and
preserving (a) the historical _context / _dbSet protected aliases
used by the six Auth concrete repositories and (b) Auth's narrower
Update semantics (Entry().State = Modified for root-only) so
Include(u => u.RefreshTokens) flows do not accidentally overwrite
refresh-token states.
Messaging.Infrastructure.Persistence.Repositories.BaseRepository<T>:
- [Obsolete] adapter with no in-tree extenders. Preserved only for
out-of-tree consumers; will be removed in the next release.
OutboxRepository<TContext> (BuildingBlocks):
- Canonical polling query matches INV-COMM-3a: Pending OR
(Failed AND NextRetryUtc <= now). DeadLettered is terminal and
never re-picked.
- The previous per-service queries had three different (and partially
buggy) interpretations: Auth picked up Pending only, Messaging
keyed retries on RetryCount<3 ignoring backoff, Category was
correct. All three now share one implementation.
Category DI now registers the canonical OutboxRepository<CategoryDbContext>
directly. Auth/Messaging/Category legacy OutboxRepository classes
remain as [Obsolete] thin pass-throughs to the canonical for one
release so any code outside this repo that referenced them keeps
working with the corrected polling semantics.
Test: new CanonicalOutboxRepositoryTests asserts the canonical polling
predicate (Pending + Failed-due, ordered, DeadLettered excluded) and
the DeleteProcessed cut-off behaviour directly — independent of any
service-side adapter, so it survives the eventual adapter deletion.
Directory.Build.props: CS0612/CS0618 moved into WarningsNotAsErrors.
Deprecation cycles must show up in the compiler output for reviewer
visibility but must not break CI between the commit that adds
[Obsolete] and the commit that removes the last caller (typically
one release apart per the T2.3/T2.4 consolidation plan).
https://claude.ai/code/session_01B59DWTDTzpx4yRCLYZEhgQ
…ashboard AbortController (T2.8 + H12 follow-up) Phase 2 T2.8 + Phase 1.5 H12 closure: T2.8 — Per-segment loading.tsx + error.tsx for tasks, dashboard, categories, profile. Closes the "blank screen during streaming / unrecoverable error" gap the audit flagged (P2-NO-LOADING-STATES-001). A shared SegmentError component (components/ui/segment-error.tsx) owns the retry + back-to-dashboard layout so each error.tsx is a 3-line client-component shim. Loading screens render skeleton rows that match the page's eventual content. H12 follow-up — Dashboard mount-time fetch trio now plumbs an AbortController through fetchTodos / fetchStats / fetchCategories. Promise.all runs them in parallel; cleanup aborts. axios.isCancel + signal.aborted guard every catch and every setState past an await, matching the tasks-page pattern shipped in wave F. Categories and profile pages were also reviewed for the same race: their fetch chains are simpler (single mount-time fetch wrapped in its own effect) and the existing error handling is adequate. They benefit only from the new loading.tsx / error.tsx. Note on T2.7 (per-route force-dynamic removal): deferred. The current nonce-based CSP requires per-request rendering for every route that includes Next.js framework bootstrap scripts (which is every route). Lifting force-dynamic safely needs either (a) a per-route nonce strategy or (b) accepting 'unsafe-inline' in script-src — both meaningful trade-offs that warrant a dedicated ADR before the change. Tests: 360/360 green; type-check clean. https://claude.ai/code/session_01B59DWTDTzpx4yRCLYZEhgQ
…guard, gRPC trust-context pin (T3.10, T3.11)
Phase 3 T3.10 + T3.11:
T3.10 — Frontend security-header expansion:
- Referrer-Policy: strict-origin-when-cross-origin → strict-origin.
The previous policy sent the full pathname + query to external sites
on outgoing navigations (e.g. a todo description containing a URL
with a token). strict-origin sends only the origin on cross-origin
and full URL on same-origin — internal analytics keep the path,
third parties get only the scheme + host.
- Permissions-Policy expanded from 3 deny rules to 22, covering every
sensitive browser API surface (payment, usb, vr, screen-wake-lock,
publickey-credentials-get, browsing-topics, etc.). Planora uses
none of them today; explicit deny narrows the attack surface for
compromised third-party scripts.
- Cross-Origin-Opener-Policy: same-origin — isolates this top-level
window from unrelated cross-origin windows (Spectre-class leak
mitigation, hardens postMessage flows).
- Cross-Origin-Resource-Policy: same-origin — declares the page's
resources are not intended for cross-origin loading.
T3.11 — Gateway forwarded-headers + gRPC trust-context audit:
- The gateway now registers UseForwardedHeaders ONLY when
ForwardedHeaders:KnownProxies contains at least one entry. With an
empty list (default), external clients cannot spoof X-Forwarded-For
to poison the rate-limit partition key. Production deployments
behind Fly must configure the Fly edge range explicitly.
- UseForwardedHeaders runs BEFORE HttpsRedirection so the latter sees
the true client protocol and does not double-redirect HTTPS edge
traffic.
- New ServiceKeyInterceptorTests::
ClientInterceptor_DoesNotLeakAuthorizationHeaderIntoOutgoingMetadata
pins INV-AZ-6: outbound gRPC metadata contains exactly x-service-key
and never the inbound HTTP Authorization or Cookie. Keeps the trust
contexts (user JWT vs peer-service identity) cleanly separated.
INVARIANTS.md — two new closed-form rules:
- INV-AZ-6: gRPC client never propagates inbound HTTP credentials.
- INV-AZ-7: gateway processes X-Forwarded-* only when KnownProxies is
configured.
Tests: 360/360 frontend; type-check clean.
https://claude.ai/code/session_01B59DWTDTzpx4yRCLYZEhgQ
planora.cache.operations{prefix,outcome} counter emitted on every
CacheService.GetAsync call. Outcomes: hit_l1 (in-process MemoryCache),
hit_l2 (Redis), miss, error. Prefix is the first colon-delimited
segment of the cache key — entity name when callers use the
CacheKeyBuilder.ForEntity<T>(id) convention, so cardinality is bounded
by the codebase's entity set. Defence-in-depth: prefixes >48 chars or
empty collapse to "_other_" so a future buggy callsite cannot blow up
the time-series cardinality budget.
Hit ratio is derived in the metrics back-end with a Prometheus query:
sum by (prefix) (rate(planora_cache_operations_total{outcome=~"hit_.*"}[5m]))
/
sum by (prefix) (rate(planora_cache_operations_total[5m]))
Tests: CacheServiceMetricsTests pins (a) miss emission when L2 empty,
(b) hit_l1 after a Set with local cache on, (c) hit_l2 with local cache
off, (d) the unbounded-prefix fallback to "_other_". MeterListener
subscribes to the published instrument and records every measurement;
runs against an in-memory IDistributedCache so no Redis required.
Documentation:
- docs/caching.md "Observability" section rewritten with the metric
definition + the Prometheus hit-ratio query; the "future work" note
deleted.
- INVARIANTS.md INV-OBS-6 instrument list extended.
Closes the open question in docs/caching.md flagged at the audit; the
master plan's T4.3 line item is now complete.
https://claude.ai/code/session_01B59DWTDTzpx4yRCLYZEhgQ
Adds N1SentinelInterceptor at BuildingBlocks.Infrastructure.Persistence
plus the INV-PERF-1 invariant that pins it to the integration test
contract.
How it works:
- The interceptor hooks every EF Core command lifecycle (Reader /
NonQuery / Scalar, sync + async). Each command is fingerprinted by
stripping parameter placeholders ($N, @pn) and collapsing whitespace,
so per-row reads collapse to a single SQL shape.
- Recording is gated by an AsyncLocal scope. Outside BeginScope() the
interceptor is a complete no-op — production runtime cost is zero.
- Inside a scope, fingerprints whose repeat count exceeds the threshold
raise N1SentinelException on dispose. Tests wrap the request under
test in BeginScope; a real N+1 in the handler fails the test
deterministically.
- Whitelist substrings exempt legitimately repeated reads. Callers
declare intent by name, not by removing the gate.
- Custom onViolation callback supports shadow-mode rollout: collect
and report violations without throwing.
Test coverage (8 tests, all green):
- 6 reads, threshold 4 → throws.
- 5 reads, threshold 5 → passes.
- Three distinct shapes ×2 each, threshold 3 → no shape crosses, passes.
- 8 reads of a whitelisted shape, threshold 3 → passes.
- 1000 reads with no active scope → no-op.
- Custom onViolation callback collects instead of throwing.
- Fingerprint normalisation (placeholders + whitespace).
- Nested scopes restore the outer scope cleanly after an inner throw.
Integration suites will adopt the gate per request handler in a
follow-up commit. The N1Sentinel is registered via DbContext options:
options.AddInterceptors(new N1SentinelInterceptor());
INVARIANTS.md additions:
- New "Performance" section + INV-PERF-1 binds the sentinel to the
request-scoped data-path test contract.
- INV-CI-3 updated to reflect the wave-B / wave-H tightening:
npm-audit threshold high, Trivy fail-on-high pass.
https://claude.ai/code/session_01B59DWTDTzpx4yRCLYZEhgQ
…L autobuild, nuget vuln auto-PR (T3.8/T3.9/T4.4/T4.7) T4.7 — RabbitMQ publisher confirms + mandatory publish. RabbitMqEventBus.PublishAsync now creates its channel with CreateChannelOptions(publisherConfirmationsEnabled: true, publisherConfirmationTrackingEnabled: true) and publishes with mandatory=true. The combination guarantees that PublishAsync's task completion equals broker durability commitment: nacks throw and unroutable messages no longer silently disappear. Outbox processor relies on this — a successful PublishAsync return is now an honest signal to mark the outbox row Processed. T4.4 — Connection pool sizing baseline. docker-compose connection strings now carry Maximum Pool Size=10 and Connection Idle Lifetime=60 for every per-service Postgres database. With 6 services × 10 connections × N replicas, the math stays under Neon-free's 100-connection cap with headroom for the migrator and autovacuum. .env.production.example and deploy/fly/.env.fly.example mirror the convention with a comment block explaining the math so operators bumping replica count know to revisit the limit. appsettings.json local-dev defaults (MaxPoolSize=100) are left alone — they target a single-developer machine where collisions are not a concern. T3.9 — CodeQL build-mode: csharp now uses autobuild (was none) so the data-flow taint queries that need compiled IL actually run. javascript-typescript stays buildless. The matrix expanded to per- language build-mode so the two languages don't share a setting that suits neither. Timeout bumped 20 → 30 min to absorb the compile step; setup-dotnet with cache: true keeps the second-build restore cheap. T3.8 — Nightly NuGet vulnerability auto-PR workflow. Compensates for Dependabot being disabled for the NuGet ecosystem (CPM + per-project PR fan-out). The new .github/workflows/nuget-vuln-pr.yml runs `dotnet list package --vulnerable --include-transitive` at 03:00 UTC daily; on a hit it opens (or updates) a single tracking PR on a stable security/nuget-vuln-tracking branch with the report body. The PR is explicitly a tracking artefact — maintainer applies the version bump in a separate PR against Directory.Packages.props. A clean scan closes the tracking PR automatically. Concurrency group prevents overlapping runs. Frontend tests: 360/360 green; type-check clean. https://claude.ai/code/session_01B59DWTDTzpx4yRCLYZEhgQ
…rs (T4.8, T4.11)
T4.8 — Redis maxmemory + allkeys-lru in docker-compose.
The local Redis container previously had no memory cap; once a
runaway prefix filled it, the container OOM'd. Now: maxmemory=256mb
plus maxmemory-policy=allkeys-lru evicts the least-recently-used keys
under pressure so the cache stays bounded. AOF persistence is kept
on so session data and rate-limit counters survive container
restarts. Production hosts (Upstash / Fly Redis) size via the
provider plan and the directive is moot there; this only affects
local dev and CI integration runs.
T4.11 — Remove unoptimized from next/image avatars.
Avatar component dropped the unoptimized flag so /_next/image now
resizes + reformats the 64/128/512 px WebP variants to the actual
display size (sizes={`${size}px`} prop). For a typical 40 px UI
avatar this is roughly a 20× bytes-on-the-wire reduction vs serving
the source variant as-is. remotePatterns in next.config.js already
whitelists the API origin in production (and all HTTP/HTTPS hosts in
dev), so the optimizer proxy can reach the avatar URL. The existing
onError handler still falls back to the initials block if the
optimizer pipeline ever fails.
Test fix: todo-small-components avatar render test now asserts a
substring of the URL-encoded optimizer src instead of the bare
resolved URL — pinning the exact serialization was a brittle
assertion that Next.js's optimizer URL shape can change between
minor versions.
Frontend tests: 360/360 green; type-check clean.
https://claude.ai/code/session_01B59DWTDTzpx4yRCLYZEhgQ
… bundle Wires actions/attest-sbom into the security workflow so the frontend CycloneDX SBOM is signed via the GitHub OIDC token and registered on the public Rekor transparency log. Downstream consumers can verify the supply-chain inventory with `gh attestation verify --owner 4Keyy planora-frontend.cdx.json`. Backend SBOMs (per-project, emitted by the CycloneDX .NET tool) are not attested here — the follow-up CD pipeline will issue one attestation per built container image, which is the right granularity. The frontend bundle is the only public-facing artefact today, so signing it satisfies the T3.7 audit line. Runs only on `push` so external-fork PRs do not consume an OIDC token they cannot use.
…livery + Outbox schema Adds the persistence layer for the Realtime service so notifications survive pod restarts (master plan T2.5, Phase 2). This is the **additive scaffold** half of T2.5 — the EF migration itself and the NotificationService rewire land in a follow-up commit that requires `dotnet ef` tooling. Scope of this commit: * `Planora.Realtime.Domain.Entities.Notification` — durable record of every consumed NotificationEvent, deduplicated by SourceEventId (unique index). * `Planora.Realtime.Domain.Entities.NotificationDelivery` — per-recipient delivery state (Pending → Delivered | NotConnected | Failed) decoupled from the parent so reconnect-replay is cheap. * `Planora.Realtime.Infrastructure.Persistence.RealtimeDbContext` with the two entities + OutboxMessages table (canonical shape matching sister services). Same domain-event dispatch pattern as CategoryDbContext. * `Planora.Realtime.Infrastructure.Persistence.Configurations.*` — EF entity configurations including the SourceEventId uniqueness, per-user indices, global soft-delete filter, and the standard OutboxMessage indices. * `Planora.Realtime.Infrastructure.DesignTime.RealtimeDbContextFactory` so `dotnet ef` commands resolve the context without booting ASP.NET. * `tools/Planora.Migrator/Program.cs` registers the `realtime` service in the one-shot migration runner; csproj reference added. * `Planora.Realtime.Infrastructure.csproj` adds EF Core + Npgsql parity with sister services. * `DependencyInjection.cs` conditionally registers the DbContext on `ConnectionStrings:RealtimeDatabase`. Test and dev hosts without the DB still start clean; production wiring activates when the migration ships. * `OutboxRepository<RealtimeDbContext>` (canonical, T2.3) registered when the DbContext is registered; no per-service duplicate exists for Realtime. * DbContextCheck health probe registered when the DbContext is registered. * docker-compose connection string left commented with an explanatory note — flipping it on without the schema applied would crash startup. * New INV-DATA-5 codifies the durability contract. Deferred to the next T2.5 commit (requires `dotnet ef`): - `InitialRealtimeSchema` migration files + ModelSnapshot. - NotificationService rewire (persist-before-push, idempotent on replay). - docker-compose connection string activation.
Extends INV-AUTH-4 to spell out which future handlers must rotate the security stamp (role assignment/revocation, admin force-logout, manual lock, admin email override) and pins the policy with a source-file contract test so a regression cannot slip past CI. * `SecurityStampUsageContractTests` — scans every `*CommandHandler.cs` under `Services/AuthApi/Planora.Auth.Application/Features/**/Handlers/`. Any handler whose constructor injects `ISecurityStampService` must also invoke `SetStampAsync` somewhere in its body; missing call → fail. A sanity assertion catches regex drift so the test cannot become vacuous. * `docs/INVARIANTS.md` — INV-AUTH-4 rewritten with three sections: shipped rotation points (now 7, including the refresh-reuse path from INV-AUTH-6), the forward-looking policy, and the explicit opt-outs (profile updates, single-session revocation). * `docs/auth-security.md` — table updated; new "Forward-looking rotation policy" subsection mirrors the invariant and cites the contract test. * `tests/Planora.UnitTests/Architecture/ArchitectureTests.cs` — adds `Planora.Realtime.Domain` to the enforced no-infrastructure-dependency set so the new T2.5 entities are covered. No production code changes: `UpdateUserCommandHandler` (profile-only) and `RevokeSessionCommandHandler` (user-scoped single-session) are deliberate opt-outs and now documented.
Closes the two scoped halves of master-plan T4.10 that don't require a wider bundle refactor. * `MotionPreferencesProvider` wraps the root layout with a single `MotionConfig reducedMotion="user"`. Every nested framer-motion component now honours the OS prefers-reduced-motion preference automatically — transforms/physics collapse, opacity/colour stay. No per-component useReducedMotion() needed. Closes the gap for `loading.tsx` and `celebration.tsx` which animated transforms unconditionally. * `color-bends-layer.tsx` — `useAdaptiveIterations` picks 1/2/3 fragment-shader iterations from navigator.hardwareConcurrency (≤2 / 4–7 / ≥8 cores). Cuts low-end mobile GPU load in half versus the previous hard-coded 2 while giving desktops a richer effect. Returns 1 during SSR so hydration is deterministic; runtime upgrade happens silently on mount. * `color-bends.test.tsx` — parameterised smoke test pins that the layer keeps rendering across all three core-count buckets. Deferred: full dynamic-import of framer-motion per route — that is a larger refactor than this commit, tracked in the master plan.
Adds the Postgres-side backstop for the per-service Npgsql pool. A leaked DbContext or a client crash mid-transaction would otherwise hold a connection open indefinitely, starving the pool (`Maximum Pool Size=10`, T4.4) and surfacing as cascading timeouts on unrelated endpoints. 30s leaves headroom for legitimate long batches while bounding the starvation window. * `docker-compose.yml` — postgres command adds `-c idle_in_transaction_session_timeout=30000`. * `deploy/fly/README.md` — new "Postgres tuning" section documents the one-time `flyctl postgres config update` command for Fly Postgres.
…n flow First slice of master-plan T2.6 (Phase 2). Lands the scaffolding plus the login flow; remaining critical flows (register UI, forgot-password, reset-password, verify-email-link, todo CRUD, sharing/hidden, profile, 2FA) land as additional `*.ui.spec.ts` files using the same scaffold. * `playwright.config.ts` — two projects: `api` (existing request-context tests, `*.api.spec.ts`) and `ui` (new, Chromium + Desktop Chrome device, `e2e/ui/*.ui.spec.ts`). Independent base URLs, both coexist. * `e2e/ui/_helpers.ts` — `requireFrontendReachable` skips the suite if Next.js is not up; `registerVerifiedUser` reuses the API path for setup so UI specs don't re-drive registration; `submitLoginForm` locates by visible label. * `e2e/ui/auth-login.ui.spec.ts` — happy-path login routes to `/tasks` with the user's name in the navbar; wrong-password keeps the user on the login page with the error banner visible. * `.github/workflows/e2e.yml` — installs Chromium, builds the frontend (production, not dev), starts `next start` on :3000, waits for ready, runs both projects, cleans up the PID. `E2E_FRONTEND_URL` exported. * `e2e/README.md` — operator doc on the two-project setup. The scaffold is additive: any CI matrix entry that doesn't export `E2E_FRONTEND_URL` falls through to API-only (UI specs gracefully skip).
Second UI spec on the T2.6 scaffold: drives the registration form end-to-end through the real Next.js render and asserts the post-submit redirect. A second scenario exercises the Zod confirm-password mismatch and pins that the page does NOT route away on a validation failure. * `e2e/ui/auth-register.ui.spec.ts` — happy path types the form fields by visible placeholder and submits; mismatch scenario pins that submission is short-circuited client-side. Validation behaviours (weak password, missing fields) stay in unit tests against the Zod resolver — this spec focuses on the *browser submission contract* so a future regression on form wiring is caught immediately.
Two more UI flows on the T2.6 scaffold. * `e2e/ui/auth-forgot-password.ui.spec.ts` — happy path types a registered email and asserts the success banner replaces the form. Anti-enumeration scenario submits an unknown email and pins that the *same* success banner appears, so the UI cannot leak whether an account exists. * `e2e/ui/tasks-page.ui.spec.ts` — verifies post-login arrival on `/tasks`, opens the create-task panel via its aria-labelled toggle, fills the title input, and closes the panel. Full create-flow validation (category selection) lands in a dedicated follow-up spec so this one stays robust against category-UI churn.
…g config
Targeted index improvements landing as EF entity configurations.
* All four outbox tables (Auth, Category, Messaging, Realtime) gain
`HasIndex(Status, NextRetryUtc, OccurredOnUtc).HasFilter("Status IN
('Pending', 'Failed')")` named `ix_outbox_messages_active`. Directly
covers the canonical polling predicate in
`OutboxRepository<TContext>.GetPendingMessagesAsync`. Excluding the
terminal `Processed` and `DeadLettered` rows keeps the index small
even when the table accumulates ahead of the cleanup sweep.
* Messaging `OutboxMessageConfiguration` added — Messaging declared the
`OutboxMessages` DbSet but never applied an explicit configuration, so
EF used defaults and the outbox table had no non-PK index. The new
config matches sister services exactly.
* `MessagingDbContext.OnModelCreating` now calls
`ApplyConfigurationsFromAssembly` so future entity configurations are
picked up automatically, matching the Auth/Category/Todo pattern.
* `TodoItemComment.AuthorId` gains a non-unique index. Audit views and
account-deletion cascade scans previously seq-scanned this column.
* New INV-COMM-5 codifies the partial-index convention so future
services holding an outbox table inherit the pattern.
Migration files generate when the next `dotnet ef migrations add` runs
against a developer environment with EF tooling available. The
schema-drift guard (INV-FLOW-5) ensures the desync surfaces as a hard
stop rather than silent partial application.
Closes the open question called out in master plan T2.7 ("Needs ADR on
CSP nonce trade-off") and the audit finding P0-FORCE-DYNAMIC.
* New ADR-0006 walks the fork in the road (static prerender + per-request
nonce is impossible by construction; hash-based CSP is the only unblock
without weakening script-src), documents the decision to keep
force-dynamic + nonce until one of three sunset conditions ships
(hash-based CSP wiring, a Next.js minor with a stable hash manifest, or
a vetted community plugin), and rejects the alternatives:
- 'unsafe-inline' — regression in security posture.
- per-route opt-in — every route boots the framework runtime, so the
set of nonce-free routes is empty until hash-CSP lands.
- hand-rolled hash pipeline today — too tightly coupled to Next.js
internals; breakage mode (white page on deploy) is unacceptable for
a single-maintainer project.
* `layout.tsx` comment on the force-dynamic line now points at the ADR
so future contributors see the rationale at the call site.
P0-FORCE-DYNAMIC is reclassified from "fix immediately" to "open
contingent on hash-CSP work" in the tracking.
Two threads in one commit since they're additive and unblock each other in CI (the profile-update spec depends on the reset-password loop being validated). **T3.6 IDOR coverage baseline.** * `docs/security-idor-coverage.md` (new) — hand-curated map of every `[Authorize]` endpoint with a resource-identifier path param. Pairs each with the IDOR protection mechanism (owner check, viewer filter, friend gate, role gate) and pins the verifying test. Zero `gap` rows in the current pass. * `INV-AZ-8` — codifies the contract that future authorized endpoints must update the table + ship an explicit cross-user test. * Forward step (auto-generation per T3.6) lands once T2.1's OpenAPI source-of-truth ships; this table is the curated interim baseline. **T2.6 cont. — reset-password + profile-update UI specs.** * `e2e/ui/auth-reset-password.ui.spec.ts` — end-to-end forgot → reset → login loop. Triggers the reset via API, scrapes the auth-api logs for the reset token (disambiguated from the verification token by matching the `Reset` subject in the log line), opens the reset page with the token in the query string, sets a new password, signs in with the new password to prove the rotation took effect. * `e2e/ui/profile-update.ui.spec.ts` — log in, navigate to /profile, rename via first-name, reload to confirm persistence. * `e2e/ui/_helpers.ts` — adds `requestPasswordResetAndCaptureToken`.
…mmits
Five defects in commits I shipped this session, fixed in one pass:
1. **T2.5 — Realtime DI startup crash on configured DB.** RealtimeDbContext
takes IDomainEventDispatcher in its constructor (parity with sister
DbContexts), but Realtime API does not call AddBuildingBlocksInfrastructure
so the service was never registered. With ConnectionStrings__RealtimeDatabase
set, the DI container would have failed to resolve the DbContext on
startup. Fix: register a NoOpDomainEventDispatcher inside the conditional
block (Realtime entities emit zero domain events today; TryAddScoped
leaves room for a future upgrade if domain events get added).
2. **T3.7 — SBOM attestation subject-digest mismatch.** Passed
`sha256:${{ github.sha }}` which is a 40-char git SHA, not a 64-char
SHA-256. actions/attest-sbom would have rejected at runtime. Fix:
switch to `subject-path` so the action computes the SBOM file's own
SHA-256, with a comment explaining the path will tighten ("attest the
built artefact, SBOM as predicate") once a single bundle exists to
digest.
3. **T4.10 — Double WebGL scene build on mount.** useAdaptiveIterations
used `useState(1) + useEffect(setIterations)` — the first render shipped
`iterations=1`, the effect upgraded it, and the WebGL renderer (which has
`iterations` in its useEffect deps) rebuilt the scene a second time. Fix:
move the detection into the useState initializer so the first render
already has the final value.
4. **T3.5 — Contract test regex too narrow.** Scanned only
`*CommandHandler.cs`, so future handlers with other naming (`*Handler.cs`,
query handlers) would slip past. Fix: broaden the glob to `*Handler.cs`.
5. **T3.6 — IDOR coverage doc cited fabricated test names + had a real
gap.** The doc named tests like `FriendshipAcceptHandlerTests` that
don't exist; the actual file is `FriendshipHandlerTests.cs` with
combined transition tests. Also flagged a real gap on
`DELETE /users/sessions/{tokenId}` — no cross-user xUnit existed for
the `RevokeSessionCommandHandler` Forbidden path. Fix: rewrote the
doc to cite real test file paths + named test methods; added the
missing `RevokeSession_WhenTokenBelongsToAnotherUser_ReturnsForbidden`
test to UserSecurityHandlerTests with explicit Update/SaveChanges
verification that the victim's token is untouched. IDOR doc now shows
zero gaps.
* `e2e/ui/auth-verify-email.ui.spec.ts` — two scenarios. The link-click path opens `/auth/verify-email?token=<real>`, the page auto-submits via its useEffect, and the success state with the "Go to dashboard" CTA replaces the form. The invalid-paste path types a bad token and pins that the form stays mounted for retry (no success banner appears). * `e2e/ui/_helpers.ts` — adds `registerUserAndCaptureVerificationToken`: registers a fresh user via the API gateway, scrapes the Auth-API logs for the verification token, returns the token **uncomsumed** so the UI spec can drive verification through the actual page. * `docs/INVARIANTS.md` — INV-DATA-5 rewritten to be explicit that this branch ships only the **scaffold** (entities, DbContext, configs, migrator wiring, conditional DI). The behavioural rewire of `NotificationService` and the initial EF migration land in a follow-up commit that requires `dotnet ef`. The previous wording implied the invariant was already enforced — it is not.
…ed reduced-motion `Toaster` (`@/components/ui/toast`) renders `motion.div` slide/fade animations for every toast. In the previous T4.10 commit it sat below the `MotionPreferencesProvider` boundary, so the global `reducedMotion="user"` setting did not apply to its animations — users who set `prefers-reduced-motion: reduce` at the OS level still saw the slide-in transitions. Move the Toaster inside the provider. Comment in `layout.tsx` updated to explain the placement (Toaster inside; ColorBendsLayer outside because it owns its own WebGL `prefers-reduced-motion` check).
Previous comment said the dispatcher "upgrades" via TryAddScoped, which was misleading — `TryAddScoped` only adds if no registration exists, it does not replace one. The actual semantics are: * If `AddBuildingBlocksInfrastructure` is called BEFORE `AddRealtimeInfrastructure`, the BB real dispatcher is already registered and the TryAddScoped here is a no-op. * If `AddRealtimeInfrastructure` runs first, NoOp gets registered, and a later `AddScoped` from BB appends — last-one-wins on resolution picks the real dispatcher. Either ordering yields the right runtime resolution; the comment now spells that out instead of claiming an "upgrade" that wouldn't have worked.
…OR map
Reality-checked the coverage map against the actual controllers:
* Auth API rows used `PATCH /users/{userId}` / `DELETE /users/{userId}` /
`POST /users/{userId}/avatar` — wrong paths. Real routes are `/me`,
`/me`, `/me/avatar` (self-scoped, no path parameter). Updated.
* Added missing `GET /users/{userId:guid}` / `GET /users` /
`GET /users/statistics` (all admin-only) so the role-gated surface is
visible in the same table.
* `POST /analytics/events` added — Authorize-only, no path parameter.
* Realtime API: added the two missed routes — `POST /notifications/send`
(self-only, derived from JWT sub claim) and `GET /connections/active`
(self-only, returns the actor's own connection ids).
* Session row now lists the correct path (`/me/sessions/{tokenId}`) and
the GET sessions row points at the right test class.
No production code changed — this is doc-only accuracy fixup.
Single-document source of truth for the audit branch state. Covers: * What landed (grouped by wave, with verifier for each). * What was deliberately deferred and why (with unblocker per item). * How to verify each piece (backend / frontend / CI / operational). * All new invariants and ADRs introduced in the branch. * Honest statement of known limitations (T2.5 scaffold-only, T4.2 configs-without-migrations, T2.6 coverage gap, etc.). * Recommended next-PR sequencing for the deferred work. Linked from the master plan; reviewers can open this one file to understand the full branch shape without spelunking through 25+ commits.
Applies the Dependabot dotnet/sdk and dotnet/aspnet 9->10 image bumps as a real
framework migration (a net9 app cannot run on the aspnet:10.0 runtime, so a
tag-only change would ship broken containers). .NET 10 is LTS; .NET 9 was STS.
WHAT changed:
- TargetFramework net9.0 -> net10.0 in Directory.Build.props and all 32 .csproj.
- Runtime-aligned Microsoft.* packages 9.0.15 -> 10.0.8 (EF Core, ASP.NET Core,
Extensions.*, Diagnostics.HealthChecks.*, Mvc.Testing); Npgsql EF provider
9.0.4 -> 10.0.2.
- Dropped explicit System.Text.Json / System.Text.Encodings.Web references: they
are framework-provided in net10 and the 10.0.7 pin was a downgrade from the
framework's 10.0.8 (fixed NU1605). Removed the unused Microsoft.AspNetCore.OpenApi
reference from all 6 service APIs (the app uses Swashbuckle; that package pulled
Microsoft.OpenApi v2 and broke Swashbuckle 6.9.0 -> CS7069). NU1510 (package
pruning advisory) kept as a non-blocking warning, matching the repo's existing
advisory policy.
- API deprecations promoted to errors under -warnaserror, fixed:
* ForwardedHeadersOptions.KnownNetworks -> KnownIPNetworks (gateway, ASPDEPR005).
* Rfc2898DeriveBytes ctor -> static Rfc2898DeriveBytes.Pbkdf2 (Auth PasswordHasher,
SYSLIB0060). Output is byte-identical (same salt, 100k iterations, SHA-512,
UTF-8), so existing stored password hashes still verify.
- All 7 service Dockerfiles + the Migrator runtime image -> dotnet 10.0 tags.
- actions/setup-dotnet pinned to 10.0.x across every workflow.
- Docs (README, architecture, codebase-map, deployment, development, getting-started,
overview, ARCHITECTURE) updated to .NET 10; CHANGELOG entry added.
HOW verified: dotnet build -warnaserror is clean (0 errors) and all 791 backend
tests pass on net10.0 (SDK 10.0.300, runtime 10.0.8).
Security: password hashing parameters and output are unchanged; only the obsolete
API was swapped for its supported static equivalent.
Refs: #67, #69, #71, #72, #73, #74, #75, #76, #77, #78, #79
Applies Dependabot PR #70 (npm-patch-minor group). All patch/minor, no API changes: - @tanstack/react-query 5.100.9 -> 5.100.14 - @tanstack/react-query-devtools 5.100.9 -> 5.100.14 - date-fns 4.1.0 -> 4.3.0 - react-hook-form 7.76.0 -> 7.76.1 - shadcn 4.7.0 -> 4.8.0 - @vitest/coverage-v8 4.1.5 -> 4.1.7 - vitest 4.1.5 -> 4.1.7 - postcss 8.5.14 -> 8.5.15 (dependency + override) HOW verified: npm install regenerated the lockfile, then lint, type-check, 370 frontend tests, and the production build all pass, with npm audit reporting 0 vulnerabilities at the high level. Refs: #70
The .NET 9 -> 10 migration swapped the obsolete Rfc2898DeriveBytes constructor for the static Rfc2898DeriveBytes.Pbkdf2. The two are byte-identical by spec, but nothing locked the stored hash format, so a future change to the salt/hash size, iteration count, algorithm, or password encoding could silently invalidate every stored credential. Added a golden-vector test: a base64(salt || hash) value computed independently of HashPassword (PBKDF2-SHA512, 100k iterations, 16-byte salt, 32-byte hash, UTF-8 password) that VerifyPassword must accept, and reject for a wrong password. This guarantees hashes stored by older builds still verify and pins the format against accidental drift. Full PasswordHasher suite (5 tests) passes on net10.0. Security: regression lock on the credential storage format; no behavior change.
After the net10 migration, running Start-Planora-Local.ps1 on a machine whose default dotnet is still .NET 9 failed the backend build with NU1202 (net10 packages incompatible with net9). Two changes make the net10 requirement explicit and self-healing: - global.json pins the SDK (10.0.100, rollForward latestMajor). A machine without a .NET 10 SDK now gets a clear 'install the 10.x SDK' error from any dotnet command in the repo instead of a confusing NU1202 mid-build. CI is unaffected: setup-dotnet installs the latest 10.0.x, which latestMajor accepts. - Start-Planora-Local.ps1 resolves a .NET 10 SDK during preflight in priority order: the system dotnet if it already exposes a 10.x SDK, else a side-by-side SDK under %USERPROFILE%\.dotnet, else a one-time local auto-install via the official dotnet-install script. The resolved SDK is prepended to PATH (and DOTNET_ROOT) for this process, which the 'dotnet run' child processes inherit. HOW verified: ran the launcher from a fresh shell whose default dotnet is 9.0.306 (global.json present). Preflight reported 'Using .NET 10 SDK from ...\.dotnet', the solution built, and all 7 backend services passed their health checks. The Docker launcher is unaffected — it builds .NET inside the sdk:10.0 image and never compiles on the host.
Two user-reported bugs.
1) A task with a description showed 'No messages yet' in its Collaboration
timeline ('ветка'). The timeline is materialised asynchronously: TodoApi
publishes TaskCreatedIntegrationEvent via its outbox, and Collaboration's
consumer writes the 'created the task' system comment plus the genesis
(description) comment. The shared OutboxProcessor and Messaging's
OutboxProcessorJob polled every 30s, so opening the task within that window
showed an empty timeline even though the data was on its way. Reproduced
end-to-end (the comments are created correctly, just late). Reduced the poll
cadence to 5s — the query is indexed and Take(20)-bounded — so the timeline
and message delivery are near-live. Re-verified: ~11s after creating a task
the endpoint returns both comments (totalCount: 2).
2) The navbar avatar sat a few px above the other pill items. Its wrapper was a
block container and the child <button> defaulted to inline-block, aligning to
the text baseline. Made the wrapper 'flex items-center' so the button centers
like the flex-aligned logo and tabs.
HOW verified: backend builds and 792 tests pass on net10.0; frontend lint,
type-check, and production build pass; the timeline fix was confirmed against a
live local stack.
… live author identity Removes a cross-service data-duplication bug class. The task description was stored twice — TodoItem.Description (the card) and a 'genesis' comment in the Collaboration DB (the branch) — synced only at creation via an async event. So pre-Collaboration tasks had an empty branch, new tasks' descriptions lagged the outbox cycle, and the two edit paths could diverge. P1 — description = single source of truth (Todo): - CheckTaskCommentAccess gRPC now returns the live description + task_created_at. - GetCommentsQueryHandler synthesises the pinned 'Author's Note' from it on read (page 1 only; id = task id, author = owner) instead of reading a stored genesis. Instant, always matches the card, present for old tasks. - TaskCreatedEventConsumer no longer materialises a genesis (only the 'created the task' system comment). Removed the POST /genesis endpoint, the AddGenesisComment command/handler/validator, and Comment.CreateGenesis / UpdateGenesisContent. The read query excludes any legacy genesis rows. - Frontend edits the description on the task (PUT /todos) via a new onSaveDescription path in the edit modal; branch-feed no longer calls genesis endpoints. Removed the dead addGenesisComment API client function. P2 — author identity resolved live: - Comment.AuthorName was a stored copy of the Auth-owned name that went stale on rename. Added AuthService.GetUserProfilesBatch (name + avatar); Collaboration resolves comment + genesis author identity live (60 s cache via CachingUserService), keeping the stored name only as an offline fallback. IUserService is now profile-based (GetUserProfilesAsync) instead of avatar-only. HOW verified: end-to-end on a live local stack — a task created with a description returns the Author's Note on an immediate (0s) fetch with the author name resolved live, and editing the description on the task reflects in the branch. Backend builds under -warnaserror; 784 backend + 370 frontend tests pass on net10.0. Docs (API, features, database, architecture) updated to the new model. Refs: #84
The Create/Update/Delete/GetUserCategories handlers caught all exceptions and returned ex.Message in the failure Result, surfacing internal/DB detail (e.g. 'database unavailable') to API consumers. They now log the full exception and return a safe generic message — consistent with the other services, which let domain exceptions bubble to the sanitising global exception middleware. Found during the repository-wide audit. Updated the GetUserCategories handler test to assert the generic message and that the internal detail does not leak. Security: removes an information-disclosure vector in API error responses.
Audit follow-ups (P1 + P2). P1 - consumer idempotency (INV-COMM-4) was documented but not implemented. RabbitMqEventBus delivers at-least-once (nack+requeue on failure), but nothing deduped - the IdempotentMessageHandler/Inbox machinery was dead code, so a redelivered or restart-replayed event produced duplicate system comments (Collaboration) / notifications. The bus now dedups centrally on the stable @event.Id via IInboxRepository: skip the handler when the id is already recorded, record it after success. Graceful + defensive - a service with no inbox (or an inbox error) falls back to the previous behaviour, never worse. Added an InboxMessages table + repository to Collaboration (PK = event id); Realtime is a follow-up. Also fixed the pre-existing bug where IdempotentMessageHandler checked ExistsAsync against the PK while storing the id in MessageId (never matched) - the new InboxMessage(Guid eventId,...) ctor makes the PK the event id. P2 - AsNoTracking on CategoryApi reads. BaseRepository already does this for its generic reads, but the custom CategoryRepository did not. Added it to the read-only methods (list/get/paged); kept tracking on GetByIdAsync (load-then-update) and FindAsync (also used for fetch-then-RemoveRange). HOW verified: build clean under -warnaserror; 784 tests pass on net10.0; live stack confirmed the inbox records the processed TaskCreatedIntegrationEvent and exactly one 'created the task' system comment is produced. Docs (database.md, features.md) updated; the Collaboration inbox table is created by EnsureCreated on a fresh DB. Refs: #84
Audit follow-up. LoginHistory and PasswordHistory are append-only tables read only for display / lockout checks / password-reuse comparison — never mutated — so their read queries do not need EF change tracking. Added AsNoTracking to LoginHistoryRepository.GetByUserIdAsync / GetRecentFailedAttemptsAsync and PasswordHistoryRepository.GetByUserIdAsync. Left tracking on all token / recovery-code / user reads (RefreshToken, User, UserRecoveryCode, Friendship): those are load-then-mutate or security decisions where AsNoTracking would risk breaking the flow, for negligible gain. Build clean under -warnaserror; Auth + repository-behavior tests pass on net10.0.
Audit follow-up. The auth forms (login, register, forgot/reset password, verify-email) rendered each <label> as a sibling of its input with no htmlFor or wrapping, so screen readers did not announce the field name on focus. Now every field is programmatically labelled: the reusable register InputField wraps its control in the <label>; the inline forms use htmlFor + matching id. The icon-only show/hide-password buttons gained an aria-label so they have an accessible name. Also documented (architecture.md) the deliberate decision NOT to add inbox dedup to Realtime: it is a stateless SignalR fan-out (no DB write), so a redelivered event is at most a duplicate transient toast — persistent inbox infrastructure there would be disproportionate, and the event-bus dedup no-ops gracefully. Verified: frontend lint, type-check, 370 tests, and the production build pass.
Audit follow-up (final a11y touch-up). The category create/edit form labelled Name and Description with sibling <label>s; added htmlFor + matching id on the Input controls so screen readers announce the field. The Icon/Color pickers and the advanced-search Priority/Category button groups use labels as visual headings for custom widgets (no single native input), so they are left as-is. Verified: frontend lint, type-check, and production build pass.
The Completed Tasks page (/tasks/completed) had no way to narrow the archive by category, unlike the active /tasks feed. Replicated the exact same filtering UX here: the "F" hotkey toggles the category filter modal (ignored while typing in inputs), an active-filter chip lists the chosen categories with a one-click clear, and the keyboard hint appears until first use. Selection is read from and written to the same shared localStorage store used by /tasks, so the filter stays consistent when moving between the two pages. Filtering is applied client-side over the loaded archive page, matching the active feed behaviour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… modal Two issues in the branch/edit modal surfaced when a non-owner opened a public task. First, the priority, due-date and visibility tokens were fully editable for viewers: the lock keyed off canManageViewerCategory, which is true for shared tasks (a viewer may set their own category), so it wrongly granted write access to fields owned by the author. Each token now takes a muted flag and the popovers a readOnly flag; for non-owners the priority/date/visibility tokens render muted and open a greyed, non-interactive read-only preview on click, while the category token stays editable for viewers as designed. The obsolete disabled prop was removed from InlineTokenStrip. Second, the modal previously resized to its content; it is now a fixed size (height 90vh, capped at 880px, overflow hidden) with the branch timeline flex-filling the middle and scrolling internally, so the modal is always the same maximum size whether the branch is empty or full. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… menu The Author's Note now lives inside the scrollable branch rail and scrolls away naturally. Once it passes out of view a condensed frosted-glass bar (avatar + truncated first line + animated bounce chevron) appears at the top of the feed via Framer Motion AnimatePresence spring. Clicking the bar smooth-scrolls back to the full card and fires a violet attention-pulse CSS animation (genesis_highlight keyframe) so the note is easy to re-read from anywhere in a long branch. The compose + menu now shows two task-action items for all participants: Take into work (flips to Leave task when already in progress) and Complete task (flips to Reopen when done). Both mirror the existing join/leave/complete flow precisely without adding new API surface. The Description attachment item is now hidden for non-owners. An optimistic workOverride flag in the modal makes the in-progress header pill flip instantly on take/leave before the parent refetch propagates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ted filter plate Resolves four reported issues across the task branch and completed page. 1) fix(collaboration): editing or deleting a branch comment always failed with a 409 'record has been modified by another user'. CommentRepository overrode the base GetByIdAsync solely to add AsNoTracking(); the Comment aggregate uses PostgreSQL xmin as an optimistic-concurrency token (a shadow property captured only on a tracked read), so the no-tracking load dropped it and the UPDATE/soft-delete issued WHERE xmin = 0, matched no rows, and threw DbUpdateConcurrencyException. Removed the override so mutations inherit the tracking base. Author-only edit (Comment.UpdateContent) and the isOwn-gated edit button were already correct. 2) feat(frontend): the branch now updates live without re-opening the modal. With no realtime socket, BranchFeed polls the newest page every 5s (paused while editing) and merges by comment id, plus schedules short catch-up merges (~0.6/1.5/3s) after take/leave/complete so the asynchronously-materialised status system-comment appears within a second or two. 3) feat(frontend): the branch opens pinned to the newest message; load-earlier and description edits preserve scroll position. 4) feat(frontend): /tasks/completed shows the same standalone active-filter chip and Quick Filter plate (with Open Menu button) as /tasks, instead of burying the chip inside the completed-archive stats card. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The collapsed pinned-note bar (shown when the branch is scrolled past the full card) used borderRadius '12px 12px 0 0', so its top corners were rounded but the bottom corners were sharp. Made it a floating rounded pill instead: all four corners radius 14, a full subtle border, and a small inset from the feed edges so it reads as a tidy chip rather than a flush header. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… instantly Task-lifecycle system comments (started working / left / completed) took up to ~20s to show in the branch because the OutboxProcessor only polled every 5s, so the producing event waited out a poll tick before publishing. Added event-driven dispatch on top of the existing poll: OutboxSignal is an in-process singleton the OutboxProcessor waits on (with the 5s interval as a timeout/safety net), and OutboxNotifyInterceptor — an EF SaveChangesInterceptor on TodoDbContext — pulses it the moment a transaction that inserted an outbox row commits. The processor now wakes in milliseconds, drains a full batch in a tight loop before idling, and publishes the event; RabbitMQ consumption is already push-based, so the Collaboration system comment lands in well under a second. The signal is resolved optionally, so services that do not register it keep the previous pure-poll behaviour unchanged. Performance: branch status system-comment latency reduced from ~5-20s to sub-second Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…wners The applied-filter summary on /tasks and /tasks/completed used to render as a separate block that pushed the page layout around. Extracted a shared QuickFilterBar component where the active filter (category chips + count + clear) crossfades into a fixed-height subtitle row inside the same Quick Filter plate, so toggling a filter animates smoothly and never grows or jolts the block. Removed the duplicated inline plates plus the standalone Filter Active chip and the F hint blocks from both pages, and the now-dead hint state. Separately, the read-only date popover shown to a non-owner viewer now omits the Today/Tomorrow/+3 days/Next week quick-pick row entirely (it was previously rendered but disabled), leaving only the read-only calendar. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…us-comment catch The header pill's Leave action called onClose(), so stopping work closed the entire branch modal. Removed it: leaving work via either the header pill or the compose + menu now keeps the modal open, so the 'left the task' system comment is read in place. Also densified the post-action catch-up merge schedule (250ms..5.6s) so the status system-comment surfaces almost immediately once the signal-driven outbox dispatch publishes it, instead of waiting for the next idle poll. The remaining perceived delay against a still-running pre-change Todo API is the old 5s poll cadence — it requires the Todo service to be restarted on the rebuilt binary to pick up the instant OutboxSignal dispatch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-safe) Adds -Lan to Start-Planora-Local.ps1 so a teammate on the same Wi-Fi can open the running app with zero setup on their end. The launcher resolves the host's physical LAN IPv4 via Get-NetAdapter -Physical (which excludes VPN virtual adapters, so a split-tunnel VPN's tunnel address is never used), opens a Windows Firewall inbound rule for ports 3000 and 5132 scoped to Profile Any + RemoteAddress LocalSubnet (self-elevating once via UAC if needed), and prints the shareable http://<lan-ip>:3000 URL with VPN guidance. The frontend already binds 0.0.0.0 and getApiBaseUrl() auto-targets the gateway on whichever host served the page, so peers need no configuration. To make LAN work end-to-end in dev without per-IP wiring: the gateway's dev CORS policy now accepts loopback plus RFC1918 private-LAN origins via a bounded SetIsOriginAllowed predicate (dev policy only; production stays an explicit allow-list), and the frontend's dev CSP connect-src now allows http/https/ws/wss (mirroring the existing dev img-src). Production CORS and CSP are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion Brought the local launcher's self-documentation fully in line with its actual behaviour and refreshed the docs that describe it. Start-Planora-Local.ps1: rewrote the comment-based help (.SYNOPSIS/.DESCRIPTION/.PARAMETER/.EXAMPLE/.NOTES) to document the complete 10-step startup pipeline, every flag including -Lan, the default ports/URLs, the per-service schema bootstrap (no separate migration step), secret handling, and logs/lifecycle. Expanded the -Help usage with an option table, a URLs block, annotated examples, a pointer to Get-Help -Full, and the Docker-launcher tip. Docs: corrected the README local-dev section (it wrongly stated the launcher applies schemas through the migrator) and added a flag table + port list; updated docs/OPERATIONS.md and docs/codebase-map.md; and rewrote the docs/configuration.md LAN section to reflect that LAN sharing is now automatic via -Lan plus the dev CORS/CSP allowances and runtime getApiBaseUrl(), with only the email-link Frontend__BaseUrl still requiring the LAN IP. Verified: the script parses cleanly, Get-Help renders, and -Help exits 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tem markers Reworked the task-branch activity timeline so it looks cleaner and stays unbroken. The rail line was an absolute element anchored to the scroll viewport (top/bottom), so once the feed scrolled it stopped partway; it now lives in a content-height wrapper as a single continuous gradient line that spans every item. All markers share one geometry (RAIL_GUTTER/RAIL_CENTER) and are centered exactly on the line instead of sitting off to the side. System-event markers gained meaning: getSystemEventMeta maps each event to an icon + color (created=Sparkles/violet, started working=Zap/indigo, left=LogOut/red, completed=CheckCircle2/emerald) shown in a tinted ring on the rail, replacing the generic grey dot. Removed the dead getSystemEventColor helper, which only matched Russian phrases and always fell back to grey for the real English event sentences. Updated the CSP middleware test to assert the (intended) relaxed dev connect-src. Docs: reviewed the last 20 commits and reconciled docs/features.md with the new rail + typed markers; verified the Outbox/Inbox, signal-dispatch, LAN, and launcher docs already match the code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… text, greyscale event icons 1) The branch modal closed when leaving the in-progress status from the dashboard: handleLeave called setEditingTodo(null). It now updates the open todo in place (status / isWorking) without closing, so leaving via any path (header pill, + menu, dashboard or active feed) keeps the modal open and the 'left the task' event is read in place. 2) A very long message with no spaces forced a horizontal scrollbar in the branch. Added overflow-wrap: anywhere / word-break: break-word to the message body, the Author's Note, and system-event text so unbreakable words/URLs wrap onto the next line. 3) System-event rail markers are now greyscale and simpler: getSystemEventIcon returns a single grey marker with simple glyphs (created=Plus, started=Play, left=LogOut, completed=Check, other=Circle) instead of the previous per-event colours. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewrote README.md into a polished landing page that serves both engineers and a product audience: a centered hero, a 'Why Planora' advantages section, a feature tour, the architecture diagram + service table, and engineering principles. Added an explicit tech-stack section that links every major backend (NuGet) and frontend (npm) dependency with its pinned version, sourced from Directory.Packages.props and frontend/package.json. Expanded the configuration reference into Required + Common-optional tables grounded in .env.example (secret-generation commands included), corrected the Auth service port to 5030 (gRPC 5031), kept the Docker + Windows launcher + LAN-sharing guides, and added the full documentation index. Enabled MD041:false in the markdownlint config so the centered HTML hero header is allowed; the README lints clean with zero errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…aserror build The CI build errors in the referenced log (CS0105 duplicate using in DeleteTodoCommandHandler, and the missing AddPlanoraSwaggerGen/UsePlanoraSwagger in Collaboration's Program.cs) predate the .NET 10 migration and are already resolved in the current net10.0 code — the exact CI command (dotnet restore && dotnet build --no-restore --configuration Release -warnaserror) builds green. This commit removes the only remaining build noise: three PackageReferences the .NET 10 SDK reports as redundant via NU1510 because the shared framework already provides them — Microsoft.Extensions.Caching.Abstractions (BuildingBlocks.Infrastructure), Microsoft.Extensions.Logging.Abstractions (BuildingBlocks.Application, which already references the Microsoft.AspNetCore.App framework), and Microsoft.Extensions.Diagnostics.HealthChecks (Planora.ApiGateway). After removal the whole solution builds with 0 warnings / 0 errors even under a strict restore + -warnaserror build, not just the split CI sequence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two CI jobs were red: 1) markdownlint lints all root *.md, so the historical asterisk (*) list bullets throughout CHANGELOG.md tripped MD004 (272 violations, Expected: dash). Normalised every bullet to '-' via markdownlint-cli2 --fix; the whole tree now lints with 0 errors. 2) Global branch coverage had fallen to 84.75% (below the 85% threshold) because the QuickFilterBar component shipped without tests (5.55% branch). Added frontend/src/test/components/quick-filter-bar.test.tsx exercising the idle vs active states, the single/plural summary, the +N chip overflow, the icon / colour-fallback chip branches, and the open/clear callbacks. Branch coverage is now 85.64% and the frontend suite is 374 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Promotes the entire
developline tomain— 77 commits of work accumulated since the lastmainsync. This is a release-grade integration PR that brings together a new microservice, the.NET 10 platform upgrade, a multi-wave production audit, a large security/performance/observability
hardening pass, the task branch timeline and task-modal UX overhaul, the category-filtering
UX, accessibility fixes, the local/Docker launcher tooling (incl. one-command Wi-Fi sharing), a
comprehensive documentation refresh, and a green CI/CD + supply-chain pipeline.
Everything below is already on
developand green in CI; merging makesmainthe faithful,production-ready reflection of the project.
Type of change
Highlights
🧩 New service — Collaboration microservice
into a dedicated Collaboration service with its own bounded context, database
(
planora_collaboration), and gateway prefix (/collaboration/api/v1/comments).(
TaskCreatedIntegrationEvent,TaskActivityIntegrationEvent,TaskDeletedIntegrationEvent) viaits Outbox and exposes
TodoService.CheckTaskCommentAccessover gRPC.friendship — never reading Todo's DB, INV-OWN-1) and materialises system/genesis comments from
the Todo events through idempotent Inbox consumers (INV-COMM-4).
Planora.Migrator --backfill-collaborationstep idempotently copies legacy comments.🏗️ Platform — .NET 9 → .NET 10 (LTS)
net10.0, bumped EF Core / Npgsql / ASP.NET packages accordingly,and pinned the Docker base images to 10.0.
PackageReferences the .NET 10 SDK flags as redundant (NU1510) so the solution buildswith 0 warnings / 0 errors even under a strict restore +
-warnaserror.🔍 Production audit — waves A → J
A systematic audit hardened the platform across every layer and codified the results as closed-form
architectural invariants (
docs/INVARIANTS.md):forwarded-headers guard, gRPC trust-context pin +
x-service-keyon every hop, header hardening,clock-skew unification, CategoryApi no longer leaks raw exception messages, friendship gate on
comment reads.
the migrator, RabbitMQ publisher confirms, connection-pool sizing,
NoOpDomainEventDispatchersemantics clarified.
AsNoTracking()on append-only/read paths,outbox partial index, Postgres
idle_in_transaction_session_timeout, Redismaxmemorypolicy,cache hit-ratio metric, hardware-adaptive WebGL background +
MotionConfig.traceparent reuse, cross-tab logout, per-segment
loading.tsx/error.tsx, AbortController ondashboard fetches.
🌿 Task branch timeline & task-modal UX overhaul
"Author's Note" is synthesised on read (with live author identity), so it shows instantly and always
matches the card — even for tasks created before the Collaboration service.
condensed frosted-glass bar appears, and clicking it smooth-scrolls back with a highlight pulse.
more breaking when the feed scrolls); avatars and system-event markers are centred on it, and
system markers use simple greyscale, event-typed icons.
participants; the author-only "Description" item is hidden for non-owners.
non-owners; the non-owner date popover hides the quick-pick row), modal stays open when leaving
work (header pill, "+" menu, dashboard or active feed), and long unbreakable text now wraps.
signal-driven so the "started/left/completed" system comment lands in well under a second.
AsNoTrackingload dropped thePostgreSQL
xmintoken), and the branch now opens at the newest message.🗂️ Category filtering UX
/taskscategory filter onto/tasks/completed(incl. the F hotkey), thenunified both into a shared
QuickFilterBarwhere the active-filter summary lives inside theplate (fixed-height crossfade — no layout shift).
♿ Accessibility
password-visibility toggles, and centred the navbar avatar.
📡 Realtime & observability
Notification/NotificationDelivery+ Outbox scaffold for the Realtime service; cachehit-ratio metric; structured Serilog + OpenTelemetry across services.
🛠️ Developer experience
Start-Planora-Local.ps1andStart-Planora-Docker.ps1brought to parity: health-gated startup,graceful shutdown, .NET 10 SDK auto-resolution, and one-command Wi-Fi/LAN sharing (
-Lan) thatdetects the physical LAN IP (VPN-safe), opens the firewall (LocalSubnet only), and prints a share
URL. Full annotated
Get-Helpand usage docs.📚 Documentation
README(with linked, version-pinned package tables), plusreconciled
architecture/database/API/features/configuration/OPERATIONS/codebase-map/INVARIANTSand a runningCHANGELOG.🔁 CI/CD & supply chain
-warnaserror) + tests, frontend lint/type-check/test/build, Playwright e2e, EFmigration scripts, OpenAPI lint, markdownlint, and security (CodeQL, Trivy IaC, gitleaks, dependency
audit, signed CycloneDX SBOM). Pinned action SHAs to Node 24 runtimes and added
developtriggers.coverage gate with a new
QuickFilterBartest suite.Testing
dotnet test Planora.sln)npm run lint && npm run type-check)npm run test— 374 tests, branch coverage 85.64%)-warnaserror(0/0)Checklist
.env.exampleupdated for new env vars (LAN/CORS/email keys documented)docs/*reconciled with the code)## UnreleasedGenerated by Claude Code