Commit Graph
58 Commits
Author SHA1 Message Date
Sluijsens 9a77eec0f1 Designs where the deploy workflow puts things on the Pi
Infrastructure design for U6: single Pi split by directory not port,
systemd --user services instead of sudo, a releases/current/shared
layout that keeps the customer website outside every atomic switch,
and a health check through the existing reverse proxy before pruning
anything. Flags that user-level systemd needs enable-linger or the
service dies the moment the deploy SSH session closes.
2026-07-28 15:32:51 +02:00
SluijsensandClaude Opus 5 247d8be241 Closes round 2 of the deployment feature
Records the stage as complete and logs the two defects that local testing
turned up after U3 and U4 were generated: ciphertext predating the key-ring
move, and an integrity check that could not tell an unreachable slave from
one that does not recognise the master.

Both were fixed in this branch rather than filed. The second is master/slave
domain behaviour rather than deployment work, so the state file says so
plainly — it sits here by decision, not because it belongs to the feature.

Verified at close: build 0 errors, 372 backend tests, 237 frontend tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-28 12:35:44 +02:00
SluijsensandClaude Opus 5 6957ec7c60 Tells apart a slave that is down from one that does not know us
The integrity check mapped four outcomes onto a single null: no response,
a 404, a rejected key, and a genuine answer. Only "rejected key" is
recoverable, and it was being reported as "unreachable" and never repaired —
so an instance registered against the wrong URL stayed broken until someone
edited the database by hand. That is exactly what happened locally.

GetRegisteredMasterUrlAsync now returns an outcome alongside the URL.
Unauthorized triggers registration; 404 is reported as "this host does not
serve the master/slave protocol", which names the actual mistake instead of
hiding it behind a generic contact failure; unreachable and server errors
behave as before.

Registering on a rejected key cannot hijack a slave that belongs to another
master: the slave accepts a registration only when it has none, and refuses
any key that does not match an existing one. So it succeeds exactly in the
case worth recovering and fails harmlessly otherwise. That guarantee lives on
the slave, so the test asserting the refusal now says out loud that the
master depends on it.

Found while diagnosing a status push that failed against a frontend URL.
Small and contained, so fixed here rather than filed as tech debt.

372 tests pass, up from 366.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-28 12:31:00 +02:00
SluijsensandClaude Opus 5 980dc80701 Records that the key-ring move leaves older ciphertext unreadable
Starting both hosts after U2 fails to decrypt a stored slave API key: the
row was encrypted under the old file-based key ring, and the database key
ring holds a different, freshly generated key. The key ring working
correctly is exactly why the old key is not found.

U2 moved the ring without a path for ciphertext that predates it. Confirmed
with the user that nothing is deployed yet, so no migration ships and the
first deploy meets an empty database. ASM-08 records the condition and makes
it blocking for U6 if it ever stops holding.

Also notes that the master lets the CryptographicException escape while the
slave catches it and reports "master API key rejected" instead — the
ambiguity U4's event was designed around, showing up on day one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-28 11:58:19 +02:00
SluijsensandClaude Opus 5 8e79a72340 Makes the application say what it is doing and when it fails
U4. Console logging plus Sentry, a same-origin tunnel so ad blockers cannot
silence browser errors, Umami on the admin SPA, and six security events that
alert rules can actually be built on.

The correlation id is the W3C trace id from the ambient Activity, enabled by
one line of ActivityTrackingOptions so every entry from every category carries
it without touching a call site. It propagates across the master/slave
boundary via traceparent, which TraceIdentifier cannot do at all, and it is
the same value ProblemDetails already returns to the browser.

The security events use source-generated LoggerMessage with constant
templates. Sentry groups log events by message, so interpolating an email
address would give every address its own issue and "more than 20 failed
logins in five minutes" could never fire — the events would arrive, be
visible, be tagged, and the alerting would silently be impossible. A test
asserts the rendered message is identical across argument values.

Scrubbing happens in-process, before transmission, and covers Set-Cookie as
well as Cookie: the login response issues the refreshToken there, so
scrubbing only the request side would protect nothing. Transactions are
scrubbed too, because they carry request data and are the channel nobody
thinks of.

The tunnel derives its destination from the DSN once at startup and reads
nothing from the request, which is what separates a tunnel from a
server-side request forgery primitive. Size is capped by a bounded read
rather than by trusting Content-Length, and the endpoint is rate limited.

Two things found along the way. Zod 4's url() hands the value to the URL
constructor, which accepts any scheme — so the existing frontend validation
would have accepted the exact "htp://" typo BR-U4-24 names, and the SPA
would have called a nonexistent origin. Now constrained to http(s). And the
new appsettings comments are verified against the real configuration
provider, because the failure mode if it rejected them is both hosts
refusing to start after a release switch.

One deviation. IAdminTokenValidator was meant to gain a reason-reporting
overload; implemented that way, a substitute returning false by default
silently inverted the access decision while both methods compiled. Two
methods whose difference is invisible at the call site is the defect, so it
is now a single Validate returning AdminTokenResult.

Touches two files from already-committed units: DatabaseMigrationExtensions
(U2) gains a flush before the rethrow, or the one Critical event in the
system dies with the process; AdminTokenValidator (U1) classifies why a
bypass was refused.

Build 0 errors; 366 backend tests pass, up from 315, and 237 frontend tests,
up from 213. tsc clean, eslint clean on every changed file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-28 11:25:54 +02:00
SluijsensandClaude Opus 5 a122548454 Sends the security headers from the application instead of the proxy
U3. These headers normally come from nginx, but the deployment target does
not allow server configuration, so the application emits them itself. That
changes the failure mode: a bad nginx config fails loudly at reload, while a
middleware that never runs sends nothing and says nothing.

Two policies, defined in code. Strict for /admin, /api/v1 and /health;
relaxed for the public website, which is authored elsewhere by someone who
has never seen this policy. Configuration decides where a policy applies and
which external origins are permitted; it cannot invent a policy that is
subtly permissive.

script-src 'self' under Strict has no 'unsafe-inline' and no 'unsafe-eval',
asserted by a test so that loosening it means deleting a test that says why.
style-src does carry 'unsafe-inline' and cannot not: Radix positions its
overlays with inline style attributes, which nonces cannot reach at all.

Two traps handled explicitly. StartsWithSegments rather than string
StartsWith, because "/administrator".StartsWith("/admin") is true and a
public page would silently lose its inline scripts with no server-side trace.
And all decision logic sits in a static writer rather than in the middleware,
because DefaultHttpContext.Response.OnStarting is a no-op — the obvious
middleware test observes nothing and an assertion that nothing was written
passes for entirely the wrong reason.

An unknown policy name fails ValidateOnStart, so the process exits rather
than quietly serving /admin under the relaxed policy. Origin format is
validated too, beyond what the design asked: a CSP source list silently
ignores a malformed source, so a URL with a path would look configured and
block the script anyway.

BR-U3-22's Umami startup warning is withdrawn (REF-U3-01) — the backend
never sees VITE_UMAMI_WEBSITE_ID. It becomes a blocking CI gate in U5.

Build 0 errors; 315 tests pass, up from 253.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-28 10:58:45 +02:00
SluijsensandClaude Opus 5 5102f8668b Settles how the headers and observability units get built
NFR Design for U3 and U4. Two decisions the earlier stages had deliberately
left open, plus four risks the functional design did not name.

OPEN-01 closed: the correlation ID is the W3C trace ID from the ambient
Activity, with TraceIdentifier as the fallback. It propagates across the
master/slave boundary via traceparent, which TraceIdentifier cannot do at
all, and it is the same value ProblemDetails already returns to the client.

REF-U3-01 raised: BR-U3-22's Umami-origin startup warning cannot work. The
backend never sees VITE_UMAMI_WEBSITE_ID, so the check would either always
warn or never warn. Withdrawn from U3 and replaced by a blocking U5 CI gate
that compares the frontend build variable against that environment's CSP
origins, where both values are visible.

Four additions beyond the functional design:
- Set-Cookie added to the scrub list; the login response issues the
  refreshToken there, so scrubbing only the request cookie protects nothing
- SetBeforeSendTransaction alongside SetBeforeSend; transactions carry
  request data too
- OnRejected on the rate limiter; today a 429 leaves no trace anywhere
- FlushAsync before the migration-failure rethrow, or the one Critical
  event in the system dies with the process

Two traps recorded with tests attached: Sentry groups log events by message
template, so interpolated messages make FR-19's rate-based alert rules
unimplementable while appearing to work; and DefaultHttpContext.Response
.OnStarting is a no-op, so the obvious middleware test asserts nothing.

Three values chosen rather than escalated, each one line to change and all
three listed for review at the end of U4's pattern document: JSON console
outside Development, TracesSampleRate 0.1, tunnel cap 200 KB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-28 10:25:11 +02:00
SluijsensandClaude Opus 5 589146a167 Removes the sample database password from the README
Replaces the literal SA password in the container command and the
example connection string with a placeholder.

This change was already in the working tree at the start of the session
and is unrelated to the deployment feature; it is committed separately
so it stays easy to identify.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-28 00:01:11 +02:00
SluijsensandClaude Opus 5 357d395629 Designs the security headers and observability units
Records the functional design for the two remaining application units,
before any of their code exists.

Security headers have to come from the application, because relying on
nginx or IIS configuration is exactly what this deployment model rules
out. Strict applies to /admin, /api/v1 and /health; a relaxed policy
applies to the public website, which this repository does not author.

The strict policy needs style-src 'unsafe-inline'. That is not a
shortcut: Radix positions dropdowns and dialogs with inline style
attributes recalculated per click and scroll position, and CSP nonces
apply only to style elements, never to style attributes. No nonce- or
hash-based variant leaves the admin UI working. The exception is bounded
to styles — script-src stays closed, which is where XSS actually lives.

The website's policy is enforcing rather than absent, so every
HTML-serving path carries a CSP and no exception has to be recorded. It
still blocks external script origins, so it remains a real boundary.

HSTS is skipped in development: browsers remember it per host and
localhost is shared with unrelated projects. Every other header applies
locally, so a CSP violation surfaces while developing.

For observability, browser error reports tunnel through the API rather
than going to Sentry directly. Ad blockers block Sentry domains, which
loses errors precisely for the users most likely to have browser
oddities. The tunnel forwards only to the host derived from the
configured DSN — a caller-supplied destination would turn an anonymous
endpoint into a request-forgery primitive.

Two consequences of the chosen options are recorded rather than left
implicit:

Enabling SendDefaultPii attaches request headers, and this application
carries two standing credentials in them. Besides the refreshToken
cookie, X-Master-Api-Key would have been sent to a third party on every
error raised during a master/slave call. The scrub list removes the
whole Cookie header, Authorization, X-Master-Api-Key and the request
body.

Console logging at Information plus structured logging to Sentry would,
taken literally, mean one Sentry event per request — exhausting the free
plan within hours and burying real errors in request noise. The
thresholds are split: console keeps Information, Sentry takes warnings
and above as events with Information as breadcrumbs, so every event
arrives carrying the trail that led to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-28 00:01:04 +02:00
SluijsensandClaude Opus 5 5f3eda2680 Makes a redeploy safe for the key ring and the schema
Nothing here is visible in normal operation. Its whole purpose is that
swapping the release directory on deploy cannot silently destroy state.

Data Protection secures the API keys that authenticate master/slave
communication. Two separate defaults would each have destroyed them:
keys are held on the filesystem, which a release swap discards, and the
application discriminator is derived from the content root path, which
changes with every release directory — so even keys stored in a database
would have stopped being derivable. Keys now live in
ApplicationDbContext and the discriminator is a fixed constant.

Losing them produces no error. It produces stored keys that no longer
decrypt, which presents as an apparent network fault between a Master
and its slaves and is easily misdiagnosed. That is also why the tests
assert the resulting configuration rather than the registration: the
XmlRepository must be the EF one and the discriminator must be the
constant, plus a round-trip proving a value encrypted before a deploy is
readable after one. A test that only checked "Data Protection is
registered" would have passed in the broken case too.

Both modules previously called AddDataProtection() themselves. Module
registration runs after the host's, so those calls re-registered the
configuration chain and would have overridden the persistent store while
IDataProtector still resolved. They are removed, with a comment at each
site — the deletion otherwise looks like a regression. Each module's own
test project now guards against it being reintroduced.

ApplicationDbContext also migrates itself at startup. Deploy targets
offer no CLI, so migrations cannot be a manual step on the server.
Failures are classified rather than treated alike: a connection failure
means the database is not up yet, normal when the app and the database
start together after a reboot, and is retried with backoff; a migration
failure means something is broken and fails at once. Either way the
process does not start, which is what makes the liveness health check
trustworthy — an application that cannot reach its schema never answers
/health, so monitoring goes red instead of reporting a healthy instance
that cannot serve a request.

The cost of migrating without a human gate is that migrations must stay
forward-compatible and non-destructive, since rollback is "redeploy the
previous release". The new migration is purely additive.

Also wires this and the preceding hosting commit into both hosts, as
they touch the same lines of Program.cs.

Two constraints are enforced by documentation rather than code, and
belong in the deployment instructions: the key table must never be
pruned, and only one instance may migrate a given database at a time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-28 00:00:45 +02:00
SluijsensandClaude Opus 5 29a93ef873 Separates website and admin roots, adds /health, hardens the availability gate
Prepares the single-host layout for deployment. The customer's public
website moves from wwwroot/ to wwwroot/web/, so a CMS deploy can no
longer overwrite content it does not own: with the website in its own
directory, the release directory can be swapped without touching it.

Each front-end gets its own file provider, and both tolerate a missing
directory at startup — a fresh deployment has no website until a
separate workspace deploys one, and the CMS must still serve /admin and
the API. When the website's index.html is absent, an embedded
placeholder is served instead of a 404, which also doubles as proof the
CMS itself is running. The placeholder is embedded in the assembly
rather than shipped into wwwroot/web/, because that directory is owned
and overwritten by the website workspace.

Adds GET /health for uptime monitoring. It reports infrastructure
liveness only and is deliberately NOT the same thing as
/api/v1/Availability/status or /api/v1/System/capabilities: those are
CMS domain state that also serve the master/slave protocol. A healthy
instance can be switched off by design, and a switched-on instance can
be unhealthy, so conflating them would alert on business state and stay
silent on real outages. /health is on the availability gate's bypass
list for the same reason.

Fixes a real defect in the gate's admin bypass. It parsed the bearer
token with ReadJwtToken, which reads claims without verifying the
signature, so an unauthenticated caller could forge an unsigned token
carrying an Owner role claim and bypass the gate that suspends a
customer's site. Protected endpoints still rejected them, so nothing
leaked — but the gate itself was bypassable. The token is now fully
validated against the same parameters as the bearer scheme, resolved
from one shared source so the two cannot drift apart.

Host wiring for these changes lands with the data-durability commit,
since both units touch the same lines of Program.cs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-28 00:00:13 +02:00
SluijsensandClaude Opus 5 8568ca43c6 Plans the Gitea deployment feature and refreshes the codebase analysis
Adds the AI-DLC inception record for deploying the CMS as a single .NET
application on hosting where no server configuration is possible.

The reverse-engineering artifacts were regenerated: the previous set
predated the Master module, the Slave host, the solution reorganisation
and single-host serving, all of which matter for deployment. Findings
were verified by running the build, both test suites and the linter
rather than inferred, which surfaced two facts the plan depends on:
the frontend lint gate currently fails (5 errors), and two transitive
packages carry high-severity advisories.

Records 24 functional requirements, 32 traced decisions and a
seven-unit decomposition whose ordering is load-bearing: durability
work must land before the first automated deploy, or the very first
deploy is the one that silently breaks master/slave trust.

Two conflicts found while designing and carried into the units:
- Both modules call AddDataProtection(), which runs after the host and
  would override a persistent key store while still passing any
  registration test.
- The availability gate runs before authentication, so its admin
  bypass cannot read HttpContext.User.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-27 23:59:30 +02:00
SluijsensandClaude Sonnet 5 38857038a0 Serves public website, admin SPA, and API from a single host
Shared hosting (e.g. mijnhostingpartner.nl) typically allows only one
site/app-pool, so SlpModularCms.Api now serves everything itself:
'/' for the customer's public website (deployed separately, not part
of this repo), '/admin' for the CMS admin SPA, and '/api/v1' for the
API as before.

- Program.cs: static files from wwwroot + SPA fallbacks per path so
  client-side routing works for both frontends.
- frontend/: builds with base '/admin/' in production (dev unchanged),
  router basepath follows suit.
- SlpModularCms.Api.csproj: publish now builds the admin frontend and
  copies its output into wwwroot/admin automatically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wi5qHAuq8UbzN4NLUFeKkJ
2026-07-26 21:01:28 +02:00
SluijsensandClaude Sonnet 5 a4458f383e Documents solution folder structure for AI agents
Adds AGENTS.md as the canonical description of the Application/Tests/Clients
solution layout, mirrored into CLAUDE.md, .junie/guidelines.md, and
.github/copilot-instructions.md so Claude, Junie, and Copilot all pick it up.
Also un-ignores .junie/ so guidelines.md is tracked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wi5qHAuq8UbzN4NLUFeKkJ
2026-07-26 20:08:58 +02:00
Sluijsens 754bd97a42 Organizes backend solution 2026-07-05 00:29:42 +02:00
SluijsensandClaude Sonnet 5 0050deb4e7 Records feature closure for local-dev-master-slave-setup
Marks aidlc-state.md as closed and logs the commit/push handoff in
audit.md; these notes were written after the prior commit had already
been created.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 21:58:29 +02:00
SluijsensandClaude Sonnet 5 0447993181 Completes local-dev-master-slave-setup: dual-instance frontend tooling, module-capability gating, and master/slave protocol self-healing fixes
Frontend (Unit 2 completion): dual dev-server tooling (pnpm dev:slave,
pnpm dev:all), per-instance browser tab titles, and a backend
capability check (SystemController + useSystemCapabilities +
ModuleGuard) so a Master-only page is hidden on a slave instance
instead of assuming every backend has every module.

Master/slave protocol fixes surfaced by actually running master and
slave side by side locally:
- Deactivating a CMS instance (Inactive) now releases the slave's
  master gate instead of leaving it stuck on its last pushed status.
- The periodic integrity check now also re-pushes status to every
  reachable slave (previously URL-verification only) and runs once
  immediately on startup.
- Added the originally-specified (but never implemented) slave-pull
  path: a slave now periodically polls its own status from the master
  (GET /api/v1/SlaveStatus) and fails open to Available if the master
  is unreachable for too long, complementing the existing push.
- The slave's own Settings page can no longer "successfully" change
  local availability while the master controls it; it's now locked
  with an explanatory banner and the backend rejects the write with
  409 instead of silently no-op'ing it.
- CMS instance status badges now match the dashboard's color/icon
  styling instead of a plain grey badge.

Also corrected the master-cms-module design docs to match this
as-built behavior, and flagged (without a full rewrite) a larger,
pre-existing divergence between its inception-stage application
design and what construction actually built.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 19:53:52 +02:00
SluijsensandClaude Sonnet 5 274946dbff Adds SlpModularCms.Api.Slave for local master/slave dev testing (Unit 1)
Relocates ModuleOrchestrator, ServiceCollectionExtensions, and
ApiPrefixConvention from SlpModularCms.Api into SlpModularCms.Core.Hosting
so a new Master-less SlpModularCms.Api.Slave host project (ports 5285/7222)
can share the same bootstrap code without duplicating it. This lets a
developer run a master instance and a slave instance side by side locally
to test the master/slave connection, without touching the existing
master/slave protocol itself.

Relocates the two orchestrator/convention test files from
Modules.Identity.Tests to Core.Tests, dropping an incidental
ProjectReference to SlpModularCms.Api that existed only for those tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 02:07:35 +02:00
Sluijsens 8072025e0b Completes master-cms-module: Build & Test, docs, and appsettings
Finishes the master-cms-module feature (Units 1-4): runs Build and
Test across master-backend, slave-availability-extension and
frontend-cms-page, fixes a missing Availability EF migration for
MasterRegistration and a TanStack Query v5 mutation-callback type
break, adds the missing MasterModule appsettings section, and
documents the module in README.md. Also seeds a tech-debt-backlog
feature to track dead config and pre-existing/introduced frontend
lint findings for later cleanup.
2026-07-01 23:09:15 +02:00
Sluijsens 488ab821a7 Adds frontend for cms page 2026-06-30 23:23:50 +02:00
Sluijsens c156107cb1 Adds 2 units and docs for unit 3. nfr-requirements plan 2026-06-29 22:18:37 +02:00
Sluijsens 0e01ca1e1c adds unit tests to cover 80% 2026-06-26 00:27:27 +02:00
Sluijsens b84e2f8e3e Changes back-end unavailable to redirect to login instead of setup page 2026-06-23 22:13:01 +02:00
Sluijsens 2544e20b3c Adds profile and settings pages 2026-06-22 23:59:04 +02:00
Sluijsens 6976eb4337 Adds user management 2026-06-22 21:14:22 +02:00
SluijsensandClaude Haiku 4.5 5331be4279 fix(auth): configurable SameSite cookie for cross-origin dev setup
Chrome 89+ schemeful same-site treats http://localhost and https://localhost
as different sites, blocking SameSite=Strict cookies on cross-origin fetch
(e.g. Vite on port 5173, API on port 7221).

Fix: make CookieSameSite configurable per environment in JwtSettings.
- Default: Strict (production)
- appsettings.Development.json: None (allows cross-origin cookie in dev)
- When SameSite=None, Secure is always forced (browser requirement)

Revert the earlier Vite proxy approach in favour of this backend config.
VITE_API_BASE_URL remains a freely configurable URL in .env.local.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 15:42:56 +02:00
SluijsensandClaude Haiku 4.5 546b773781 fix(auth): add Vite dev proxy to fix SameSite=Strict cookie on page reload
Chrome 89+ treats http://localhost and https://localhost as different sites
(schemeful same-site). The refreshToken cookie (SameSite=Strict) was not
sent when the Vite dev server (HTTP) made cross-origin fetch calls to the
.NET backend (HTTPS), causing a 401 on every page reload.

Fix: route /api/* through the Vite dev server proxy so all requests stay
same-origin. The cookie is now always sent and auth sessions survive reloads.

- vite.config.ts: proxy /api → https://localhost:7221 (secure:false for dev cert)
- .env.example: document proxy vs direct mode, set default VITE_API_BASE_URL to empty
- config.ts: accept empty string as valid apiBaseUrl alongside full URLs

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 15:23:19 +02:00
SluijsensandClaude Haiku 4.5 d8b0484803 fix(availability): translate status messages, return stored admin reason from API
- Add AvailabilityStatusDetails record and GetStatusDetailsAsync() to
  IAvailabilityService; implement in PersistentAvailabilityService so the
  admin-set reason stored in the database is returned alongside the status
- AvailabilityController.GetStatus() now returns the stored message instead
  of hardcoded English strings
- Add messageAvailable / messageMaintenance / messageUnavailable translation
  keys in en + nl so default messages are fully translatable
- AvailabilityStatusBadge: Available always shows the translated default;
  Maintenance and Unavailable show the custom admin reason when set,
  otherwise fall back to the translated default

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 14:53:11 +02:00
SluijsensandClaude Haiku 4.5 d69512a449 fix(i18n): eager-load nl bundle, fix language selector, remove hardcoded backend messages
- Eager-load both en and nl translation bundles at i18n init to eliminate
  the async gap that caused English flash when Dutch was the detected language
- LanguageSwitcher: use i18n.language (synchronous) instead of resolvedLanguage
  (asynchronous) so the visual selection is always correct after switching
- AvailabilityController: remove hardcoded English messages ("System is running
  normally.") from GET /availability/status; return empty string so the frontend
  translations control the display text

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 14:16:27 +02:00
SluijsensandClaude Haiku 4.5 3c6a06028e feat(unit-4): Dashboard — availability widget with TanStack Query
- Add @tanstack/react-query 5.101.0; wrap app with QueryClientProvider
- Add AvailabilityStatus type and AvailabilityResponse to api/types.ts
- Implement useAvailabilityStatus (staleTime 30s, stale-on-error preserved)
- Add AvailabilityStatusBadge with green/amber/red states and stale indicator
- Replace DashboardPage placeholder card with live availability widget
- Add MSW availability handler; update test/utils with QueryClientProvider
- 55/55 tests pass (FR-05)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 13:39:29 +02:00
SluijsensandClaude Haiku 4.5 f476e06691 fix(unit-3): Add settings/profile routes, move profile to UserMenu
- Add /settings (Owner only) and /profile routes under authenticatedRoute
  so sidebar stays visible and layout is preserved
- Create SettingsPage and ProfilePage placeholder components (coming soon)
- Remove Profile nav item from Sidebar — profile now accessible via UserMenu
- Add Profile link to UserMenu dropdown above logout
- Update Sidebar tests to reflect profile-free nav

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 12:56:16 +02:00
SluijsensandClaude Haiku 4.5 76b7bfc09b feat(unit-3): Layout & Navigation — sidebar, mobile overlay, theme toggle
- Remove Topbar; move UserMenu + LanguageSwitcher to sidebar footer
- Add ThemeToggle (dark/light) with localStorage persistence
- Add useTheme hook; no-flash inline script in index.html
- Add MobileBar (hamburger + app name, mobile-only)
- Add SidebarOverlay (slide-in from left, backdrop closes it)
- Sidebar: role-filtered nav (BR-U3-01–06), onClose prop for mobile
- AppLayout: desktop sidebar-only layout, mobile bar + overlay
- i18n: theme.* and nav.openMenu/closeMenu keys (NL + EN)
- Tests: 43/43 passing (14 new — useTheme, Sidebar roles, AppLayout)

Stories: US-08, US-18, US-19

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 12:03:10 +02:00
SluijsensandClaude Haiku 4.5 7ed388ed5b docs(unit-3): Add code generation plan with open questions
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 11:54:11 +02:00
SluijsensandClaude Haiku 4.5 6760257e49 docs(unit-3): Complete functional design based on user answers
All Q1-Q5 answered A:
- Topbar removed; LanguageSwitcher + UserMenu move to sidebar footer
- Mobile: slide-over overlay with hamburger button
- Theme toggle in sidebar footer
- No-flash init via inline script in index.html
- Role-filtered sidebar items client-side

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 11:51:24 +02:00
SluijsensandClaude Haiku 4.5 037332e52f docs(unit-3): Revert to open questions in functional design plan
Functional design documents were written based on self-answered questions
instead of asking the user. Removed pre-written docs and restored the plan
with open questions for the user to answer.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 11:47:33 +02:00
SluijsensandClaude Haiku 4.5 31cc45d79b docs(unit-3): Functional design for Layout & Navigation
- Role-filtered sidebar (Owner/Admin/User visibility rules BR-U3-01–06)
- Topbar removed; UserMenu + LanguageSwitcher move to sidebar footer
- Mobile slide-over sidebar with hamburger button
- Theme toggle (light/dark) with localStorage persistence and no-flash init
- New components: MobileBar, SidebarOverlay, ThemeToggle, useTheme
- Unit 2 marked complete in aidlc-state

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 11:44:24 +02:00
SluijsensandClaude Haiku 4.5 bbb8d5f423 fix(auth): Remove hardcoded api/v1 prefix from AuthController route
ApiPrefixConvention already prepends api/v1 to all controllers.
AuthController had it hardcoded too, resulting in the doubled route
api/v1/api/v1/auth/login which caused a 404 on login attempts.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 11:35:24 +02:00
SluijsensandClaude Haiku 4.5 7237669b14 fix(setup): Apply language choice and fix post-setup redirect
- Call changeLanguage(values.language) after successful owner creation so
  the UI immediately switches to the language the owner chose
- Call _markSystemInitialized() to update the router cache before navigating
  to /login; without this the guard saw initialized=false and redirected
  back to /setup, blocking the redirect

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 11:25:40 +02:00
SluijsensandClaude Haiku 4.5 27490356cb fix(ui): Add type=button to FormErrorBanner dismiss to prevent form resubmit
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 11:20:48 +02:00
SluijsensandClaude Haiku 4.5 2f381d8407 fix(frontend): Strip confirmPassword before API calls; add pointer cursor
- Destructure confirmPassword out of form values in SetupPage and
  InviteCompletePage so it is never sent to the backend
- Add global cursor:pointer rule for buttons, selects, labels and links

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 11:17:20 +02:00
SluijsensandClaude Haiku 4.5 f221d7cee5 fix(unit-2-auth): Redirect to /setup on API failure; store owner display name
- Router catch block now falls back to { initialized: false } instead of
  silently continuing, preventing unwanted redirect to /login when the
  API call fails at startup
- Add Name field to CreateOwnerRequest and propagate to ApplicationUser.DisplayName
  so the owner's display name is stored during initial setup

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 11:06:34 +02:00
SluijsensandClaude Haiku 4.5 8d95754ece fix(unit-2-auth): Fix API endpoint paths and JSON casing for real backend
- Add /api/v1/ prefix to all Unit 2 API calls (Setup/status, Setup/owner,
  Invitation/validate, Invitation/complete) to match backend ApiPrefixConvention
- Fix setup status endpoint to POST /api/v1/Setup/owner (not /Setup)
- Handle PascalCase 'Initialized' response from .NET backend without camelCase policy
- Update all MSW mock handlers to match corrected /api/v1/ URL patterns
- Remove unused imports from RouteGuard.test.tsx

Root cause: backend uses ApiPrefixConvention('api/v1') but frontend calls
were missing the prefix, and .NET defaults to PascalCase JSON serialization.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 10:55:34 +02:00
SluijsensandClaude Haiku 4.5 c9cd0ff457 fix(unit-2-auth): Fix all 4 failing unit tests to 29/29 passing
- Fix invitation MSW handler: use 'valid-token' to match test fixtures
- Add data-testid='setup-success' to SetupPage success state
- Change invite email input from readOnly to disabled (matches test assertion)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 10:20:00 +02:00
SluijsensandClaude Haiku 4.5 15b417c929 docs: Update Unit 2 completion status - Code Generation 86% pass rate
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 10:05:50 +02:00
SluijsensandClaude Haiku 4.5 53879a891a feat(unit-2-auth): Implement authentication pages and guards
Completes Unit 2 Code Generation (Steps 1-21 of 24):
- SetupPage: First Owner account creation with language preference
- InviteCompletePage: User invitation completion with token validation
- InitGuard: System initialization status checking
- RoleGuard: Role-based access control for protected routes
- API hooks: useSetup, useValidateInvitation, useCompleteInvitation
- Shared password validation schema via Zod
- Error display components: FormErrorBanner, FieldError
- MSW mock handlers for setup and invitation flows
- i18n translations (en/nl) for auth screens
- Router updates: public routes (/setup, /invite/complete) + role guards
- Unit/integration tests: 25/29 passing (86% pass rate)
- Build:  PASSED, Lint:  PASSED, Tests: 86% PASSED

Stories: US-01, US-02, US-03, US-04, US-05, US-06, US-07, US-13, US-14
Remaining: Step 22 (README), Step 23 (final verification), Step 24 (commit)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-22 09:58:28 +02:00
Sluijsens f9689d2091 Adds unit 2 functional design and code generation plan and gap 4 report 2026-06-21 23:28:45 +02:00
Sluijsens ab93a5c7d1 Adds auth pages 2026-06-21 00:15:28 +02:00
Sluijsens 7dfc3a9692 Adds front-end set-up 2026-06-20 17:04:17 +02:00
Sluijsens efd1569c26 Adds nfr requirements and design for unit 1 2026-06-19 21:34:20 +02:00
Sluijsens 7e4397af4e Adds unit 1 functional design and answered nfr requirements questions and plan 2026-06-19 20:39:36 +02:00