Adds a new JSON-bodied signature endpoint as a sibling of the
legacy PNG-based prosign/clisign routes. The legacy flow stays
intact: the TeX invoice templates (Bill_tex.cshtml,
Estimate_tex.cshtml) still consume the sign-{billingCode}-{id}.png
files the old endpoints write, and the new endpoint writes to a
distinct /signatures/ tree under UserFilesDirName. A future
migration commit will regenerate PNGs from the JSON payload and
decommission the PNG flow.
Scope
- New Signature entity (Yavsc.Server/Models/Billing/Signature.cs)
with FK to Estimate, FK to ApplicationUser (Signer), Type
(Pro/Client) enum, CoordinateMax (default 10_000), int[] Strokes
(native Npgsql mapping), CapturedAtUtc, FilePath. Multiple
versions per (EstimateId, Type) are allowed; the controller
reads the most recent.
- New Estimate.Signatures nav collection (InverseProperty) so the
composite index covers both sides of the relation.
- New DbSet<Signature> Signatures + composite index
(EstimateId, Type, CapturedAtUtc DESC) in ApplicationDbContext
OnModelCreating. DeleteBehavior.Cascade on Estimate deletion
cleans up signatures automatically.
- New EstimateSignatureFileHelper (Server/Helpers) with
ReceiveEstimateSignatureAsync(user, estimateId, type, payload).
Writes a yavsc.signature/v1 JSON envelope to
UserFilesDirName/{user}/signatures/sign-{type}-{estimateId}-{ticks}.json.
Quota update lives in the controller, not the helper, because
the helper has no DbContext access.
- New endpoint POST /api/bill/estimate/{id:long}/sign on
BillingController. Authz is body-driven (the bearer token is the
PostIt OAuth client, not the end user, so signerUserId is in
the JSON body, validated against Estimate.OwnerId/ClientId).
Returns 201 Created with the new Signature's metadata.
Plumbing
- SignatureSubmission (body type) lives next to BillingController
in the same file — small enough to keep colocated.
- The legacy prosign/clisign routes are untouched. They keep
the IFormFile PNG contract; the new endpoint is the JSON
counterpart.
Tests
- New EstimateSignatureFileHelperTests in Yavsc.Org.Tests
(8 tests, all green): filename format incl. lowercase type and
ticks, envelope v1 round-trip (parsed via JsonDocument, not
text matching), null payload rejected, non-positive
estimateId rejected. Disk side effects are isolated to a
per-test temp root via AbstractFileSystemHelpers.UserFilesDirName.
- Yavsc.Org.Tests full suite: 29/29 green.
- PostIt.Tests: 57/57 green (untouched by this commit).
- Builds: Yavsc.Server, Yavsc.Api, Yavsc.Org, Yavsc.Org.Tests
all compile clean.
Out of scope
- EF migration: the Signatures table doesn't exist in the
database yet. The migration is intentionally a separate
commit so the generated SQL can be reviewed against the
composite index and the int[] column type before it touches
any prod database. Until the migration lands, the new
endpoint will 500 on SaveChanges; the [DEV] button in
PostIt is the only call site, so this is acceptable.
- SignalR handler that opens the signature page on a
'devis received' push — commit 4.
Builds on b0495514 (SignaturePadControl + SignaturePadData) with a
full Avalonia page that captures signatures, renders them as Polylines,
and persists the wire-format payload to ~/.local/share/PostIt/signatures
as JSON v1.
Scope
- New SignaturePage (axaml + code-behind) hosts the render-agnostic
control: a fixed-size Border is the hit-test surface, an overlaid
Canvas is rebuilt on every RedrawRequested from the Strokes buffer.
- SignaturePageViewModel wraps the control: exposes StrokeCount /
PointCount / StatusMessage, Clear and CaptureAsync commands, and
Attach/Detach for view-lifetime ownership.
- CaptureAsync writes a JSON envelope { format, coordinateMax,
capturedAtUtc, strokes, strokeCount } to
LocalApplicationData/PostIt/signatures/signature-yyyyMMdd-HHmmssfff.json.
This is a stop-gap; the production transport will be
POST /api/signature/{devisId} on Yavsc.Api (commit 3+).
- Entry point is a [DEV] button on MainPage that pushes the page
onto the NavigationPage. The production trigger is a SignalR push
from Yavsc.Org ("devis received, sign here") landing on a hub
handler — the button and its Click handler are explicitly marked
dev-only and tracked for removal in the same commit that wires
the SignalR handler.
Plumbing
- App.axaml.cs: SignaturePage and SignaturePageViewModel registered
as Transient in the DI container.
- ViewLocator: routes SignaturePageViewModel to SignaturePage.
- SignaturePadData: adds PointCount (sum of pairs across strokes),
used by the VM status bar and the test surface.
Tests (57/57 green, 9 new in this commit)
- SignaturePageViewModelTests: constructors and dimension validation,
Attach/Detach idempotence, StrokeCompleted and Clear propagate to
the VM, CaptureAsync on empty buffer is a no-op, CaptureAsync on a
non-empty buffer writes a v1 envelope with the expected
structure (parsed back via JsonDocument, not text matching), and
creates the destination directory if missing.
- All previously-green tests (48) remain green.
Out of scope
- POST /api/signature endpoint on Yavsc.Api (commit 3).
- SignalR handler that opens the page on a "devis received" push.
- Rasterization: this commit only proves capture and persistence;
the visible ink is a Polyline reconstruction, not a PNG, by
design (per the wire-format decision in commit 1).
Note on SignaturePadData
- The PointCount property was added after b0495514 landed. It is
folded into this commit rather than amending b0495514 to keep
the existing history readable; the change is mechanical and
tested by the new SignaturePageViewModelTests.
The postit://callback re-launch crashed Avalonia inside
DataValidationErrors.SetErrors with 'The calling thread cannot
access this object because a different thread owns it'. Two
Settings instances raced on PropertyChanged: one was the DI
singleton registered by App.OnFrameworkInitializationCompleted,
the other was a freshly-constructed fallback in
LoginPageViewModel() and LoginPage.axaml.cs's DataContext-null
branch. Avalonia's binding sink caught the cross-thread
notification and crashed before the LoginPage could render.
Fix at three layers:
1. Settings: lock the mutation gate so concurrent Load() /
ApplyJson() callers cannot tear reads; override
OnPropertyChanged to marshal every notification onto the
Avalonia UI thread via a new UiDispatcher helper (no more
cross-thread SetErrors). Add BindToServiceProvider /
RequireCurrent so production code paths cannot silently
allocate a second instance.
2. LoginPageViewModel(): resolve the canonical Settings from
the DI container (Settings.RequireCurrent) instead of
new Settings(). The cross-thread crash is now caught loudly
with a clear 'Settings.Current is not bound' error if
something instantiates the VM outside a bound App.
3. HomePage.axaml.cs and LoginPage.axaml.cs: resolve the
next view-model and BlogApiClient through App.Services
instead of constructing them with 'new'. Same instance
tree as the rest of the app; the postit://callback race
disappears by construction.
MainPageViewModel and HomePageViewModel keep their existing
'?? new Settings()' fallback for test friendliness, but the
fallback is now harmless because Settings itself is
thread-safe.
Adds two regression tests in SettingsLoadTests covering
concurrent Load+mutate and concurrent idempotent Load.
- ContentPage now stretches to fill the window
- root StackPanel replaced by a 3-row Grid (Auto, *, Auto)
so the post list absorbs the middle band and the detail
panel sits below
- detail panel inner StackPanel replaced by a Grid (Auto,
Auto, Auto, *, Auto) so the AvaloniaEdit TextEditor fills
all remaining width and height; MinHeight=320 keeps a
sane floor on tiny windows
HomePage.OnLoginClick does Navigation.PushAsync(new MainPage
{ ... }). NavigationPage.PushAsync only accepts a Page (or
Page subclass), not a MultiPage. MainPage was declared as
public partial class MainPage : NavigationPage
in MainPage.axaml.cs and the root of MainPage.axaml was
<NavigationPage ...>
which means 'new MainPage()' produced a MultiPage, not a
Page. PushAsync against a MultiPage argument does not route
through the standard Page push path; the visible result is
that the login succeeds, LoginSucceeded fires, but the UI
stays on LoginPage — the user is left looking at the
post-login state without any navigation.
The XAML content of MainPage (a StackPanel with the post
CRUD UI, a ListBox, a TextEditor) does not need the
multi-page container semantics — it's a single screen.
Switch both the code-behind base class and the XAML root
element to ContentPage so that MainPage is what
PushAsync expects.
LoginAsync uses ConfigureAwait(false) on the await of
LoginInteractiveCoreAsync. Since the surrounding code is
already executing on the UI thread (it was reached via a
RelayCommand that the UI dispatcher dispatched), the
ConfigureAwait drops the SynchronizationContext, and the
subsequent setters — IsBusy, AccessToken, StatusMessage,
LoginSuccess, and the LoginSucceeded?.Invoke() — run on a
thread-pool worker.
The downstream effects are all UI-bound: PropertyChanged
events fire, the BindingEngine republishes them as
AvaloniaObject.SetValue calls, and SetValue calls
Dispatcher.VerifyAccess. VerifyAccess throws because the
AvaloniaObject was created on the UI thread (owned by it)
and the SetValue is being attempted from a thread-pool
worker. Avalonia 11.12 throws SynchronousException through
DispatcherOperation.InvokeCore instead of dispatching back,
so the X11 message loop crashes the process with
System.InvalidOperationException: 'The calling thread cannot
access this object because a different thread owns it.'
Reproduced with the freshly installed postit_1.0.0-1_amd64.deb
package on a Debian 13 host — the .NET runtime loaded the
app, Avalonia started the X11 message loop, the operator
clicked 'Se connecter', the OIDC flow reached the post-login
phase, and the post-await setter chain crashed the process.
Drop ConfigureAwait(false) so the await captures the UI
thread SynchronizationContext and the setters resume on the
UI thread. The inner LoginInteractiveCoreAsync still uses
ConfigureAwait(false) for its own await, which is fine —
the inner method does not touch observables, only mutates
Platform.CreateBrowser and awaits the OIDC roundtrip, so it
can run anywhere.
The EMaillingTests.SendEMailSynchrone smoke test asserts the
recording fake observed this exact call sequence on a successful
send:
Connect, Authenticate, Send, Disconnect
MailSender.SendEmailAsync only calls Authenticate when
smtpSettings.UserName is non-null (src/Yavsc.Server/Services/
MailSender.cs line 89). WebServerFixture built the host without
a Smtp config — so UserName resolved to null, Authenticate was
skipped, and the recording captured only:
Connect, Send, Disconnect
Pre-existing breakage, not introduced by recent work; the
fixture had been loading from .env indirectly (probably never,
or before a refactor that stopped doing so).
Feed the test host a fake SMTP config via the same
AddInMemoryCollection the fixture already uses for
ConnectionStrings:
Smtp:Host = smtp.test.local
Smtp:Port = 465
Smtp:UserName = test-user
Smtp:Password = test-pass
UserName non-null means MailSender now exercises the Authenticate
branch, which the recording captures. Tests in the Yavsc.Org.Tests
suite: 21/21 green (was 20/21 with SendEMailSynchrone failing).
- CONTRIBUTING.md 'Tests' section now describes the smoke
pattern: per-BC, in-memory TestServer, EF InMemory, asserts
2xx/3xx or 401/403 on a representative GET.
- ROADMAP.md 'Tests d'integration smoke par BC' flips from
open to ticked off (Yavsc.Org coverage), with a note that
Yavsc.Api and Yavsc.Blogs smoke coverage is left for a
future session (separate WebApplicationFactory<Program>
targets).
With this commit, Jalon 0 'Fondations techniques' is fully
ticked off. The release criterion
'dotnet build + dotnet test + docker compose up verts sur
une machine vierge (apres procedure d'install)'
is met end-to-end for Yavsc.Org; the docker-compose criterion
documents the cert/HTTPS requirement for web explicitly.
Two tests, two bounded contexts (BCs as enumerated in
doc/ddd-exploration-2026-06-14.md):
- AccountSmokeTests : GET /signin
YavscConstants.SigninPath = "~/signin"
Routing + Razor + IdentityServer + EF + DI all wired.
- BlogSmokeTests : GET /BlogSpot/Index
BlogSpotController (note the capital S) under
Controllers/Communicating/. No class-level [Route], so
conventional /{controller}/{action} applies.
Both use TestWebApplicationFactory<Program> + the EF InMemory
provider wired by WebServerFixture.SetupHost, so they boot the
production HTTP pipeline without sockets, certs or a real DB.
Closes the 'Tests d'integration smoke par BC' item of Jalon 0
in ROADMAP.md (Yavsc.Api / Yavsc.Blogs coverage to come).
Smoke tests for the Jalon 0 'Tests d'integration smoke par BC'
item need a small helper to:
- issue a GET on an in-memory test server (HttpClient built by
TestWebApplicationFactory<Program>);
- assert that the response is 2xx (page served), 3xx (redirect
to login) or 401/403 (anonymous rejected). Anything else —
404 route missing, 5xx server crash, connection refused —
fails the test.
This commit only introduces the base class. Subsequent commits
add the per-BC smoke tests (Account, Blog, etc.).
The work was actually 'centraliser les versions communes'
(shared packages), not 'centraliser toutes les versions'. That
work is done:
- Directory.Packages.props at the repo root declares shared
package versions (ManagePackageVersionsCentrally=true,
see e.g. coverlet.collector, HigginsSoft.IdentityServer8,
IdentityModel.OidcClient, Microsoft.AspNetCore.*,
xunit.v3, …);
- each product directory imports it via GetPathOfFileAbove
and adds only product-specific versions.
No remaining work justifies the bullet. Removing it from
Jalon 0 leaves one open item: 'Tests d'integration smoke par BC'.
The shorthand 'secrets: - yavsc_appsettings' relies on Compose
v2 to derive both source and target from the same name. In
some BuildKit integrations this is not enough — the secret id
seen inside the Dockerfile (yavsc_appsettings) and the source
defined at top-level (yavsc_appsettings, file: ...) end up not
being mapped correctly, leading to:
cp: cannot stat '/run/secrets/yavsc_appsettings':
No such file or directory
at the blogs-runtime / api-runtime / web-runtime stages.
Use the explicit long form:
secrets:
- source: yavsc_appsettings
target: yavsc_appsettings
in all three runtime services. source is the top-level secret
name (file: ./src/Yavsc.Org/appsettings-org.json); target is
the id BuildKit exposes inside the container at
/run/secrets/yavsc_appsettings, matching --mount=type=secret,
id=yavsc_appsettings in the Dockerfile.
After the Dockerfile refactor into multi-stage (commit 6f975f87),
'docker build .' without --target selects the LAST stage of the
Dockerfile — which is blogs-runtime, an ASP.NET image with no
APK to extract. The subsequent docker cp command then fails
with:
Error: No such container:path: …PostIt.Android/bin/Release/
net10.0-android/android-arm64/com.CompanyName.PostIt-Signed.apk
Add --target build-env to scope the build to the build-env stage
(the one that produces both the .apk and the publish artifacts,
and which still ends with CMD ["bash"]).
The 14 docker commits landed across this session achieve the
critère de sortie: 'docker compose up' starts db/api/blogs on
a bare host, and web fails with a documented IdentityServer
signing-certificate error that points at the installation
procedure (volume mount + Kestrel:Endpoints:Https in
appsettings-org.json).
Update the checkbox status and link to CONTRIBUTING.md so a
reader of ROADMAP.md can navigate to the install procedure.
Two Jalon 0 items remain open:
- ◐ NuGet centralisation (partial)
- ☐ Tests d'intégration smoke par BC
Make explicit what was implicit: 'docker compose up' is not
expected to start everything on a bare host. Yavsc.Org (web)
requires an HTTPS signing certificate for IdentityServer8 in
Production mode, and the volume mount /etc/letsencrypt:/etc/letsencrypt:ro
is the documented way to supply it.
Two related changes:
- CONTRIBUTING.md, 'docker compose up' section: spell out that
db + api + blogs start cleanly on a bare host, web fails with
the documented IdentityServer error, and that the difference
between 'vierge' and 'configured' is exactly the cert volume.
- docker-compose.yaml, web service: expand the commented volumes
block to point at the same error message and reference the
'HTTPS en production' section in CONTRIBUTING.md, so an
operator reading the compose file knows what to uncomment
and where to look.
Both appsettings-blogs.json and appsettings-api.json shipped a
Kestrel:Endpoints:Https block pointing at https://localhost:3003
and https://localhost:3004 respectively. These are leftover dev
templates that don't match the production ports (5004/5005 for
Blogs, 5002/5003 for Api) and crash Kestrel at startup when no
cert is configured for those URLs.
docker-compose.yaml now sets ASPNETCORE_URLS explicitly per
service runtime (http://+:5000/5002/5004 + ASPNETCORE_HTTPS_PORT=empty),
which is the authoritative source for the binding. The baked
Kestrel block in the appsettings overrides nothing useful and
just blocks HTTP-only startup.
Search confirms no source code references localhost:3003/3004
(grep across src/*.{cs,json,axaml,cshtml} returns only the two
lines we just deleted), so removing the block is safe.
.env contains ASPNETCORE_ENVIRONMENT=Development, which gets
loaded via env_file into every service. With ASPNETCORE_ENVIRONMENT=Development, ASP.NET Core loads appsettings-org.Development.json — which has a Kestrel:Endpoints block binding BOTH http://localhost:5000 AND https://localhost:5001. The HTTPS endpoint has no cert on a fresh host, so Kestrel crashes with:
fail: Microsoft.Extensions.Hosting.Internal.Host[11]
Unable to configure HTTPS endpoint. No server certificate
was specified, and the default developer certificate could
not be found or is out of date.
Fix: add an explicit ASPNETCORE_ENVIRONMENT=Production to each
runtime service's environment: block. In Compose, literal
environment: entries override env_file: entries with the same
name, so this wins over .env's Development value.
Production-loaded appsettings-org.json has no Kestrel block, so
Kestrel uses only the ASPNETCORE_URLS env var we already set
(http://+:5000 etc.) — HTTP only, no HTTPS crash.
Update the 'HTTPS en production' section to match the new
docker-compose layout:
- explain why the per-service environment: block pinning
ASPNETCORE_URLS to HTTP-only and clearing ASPNETCORE_HTTPS_PORT
is required (appsettings-org.Development.json sets
Site.Authority to https://localhost:5001, which makes Kestrel
auto-detect an HTTPS endpoint and crash without a cert);
- give the exact 5-step recipe to enable HTTPS in production:
switch ASPNETCORE_URLS to a double-bind form, uncomment the
HTTPS port, uncomment the /etc/letsencrypt volume mount, add a
Kestrel:Endpoints:Https block in appsettings-org.json pointing
at the Let's Encrypt fullchain.pem + privkey.pem, and rebuild
the runtime image (since appsettings are baked via BuildKit
secret mount).
The runtime services were trying to bind HTTPS even though the
compose file only mapped HTTP ports. Symptom on first start:
fail: Microsoft.Extensions.Hosting.Internal.Host[11]
Hosting failed to start
System.InvalidOperationException: Unable to configure
HTTPS endpoint. No server certificate was specified,
and the default developer certificate could not be found
or is out of date.
The root cause is appsettings-org.Development.json which sets
Site.Authority = https://localhost:5001
combined with Kestrel's auto-detection of https_port from the
listening URL. ASP.NET Core 9 promotes any 'Authority' / 'Url'
property to a Kestrel binding target unless ASPNETCORE_URLS is
explicitly set.
Fix: add an environment: block on each runtime service pinning
ASPNETCORE_URLS to the HTTP-only form, plus empty
ASPNETCORE_HTTPS_PORT to suppress auto-detection. This forces
HTTP-only binding on the 'machine vierge' criterion of Jalon 0,
where no cert is available.
For production HTTPS, the existing commented # - '5001:5001' /
# volumes: /etc/letsencrypt blocks stay the way to enable it:
uncomment the port, uncomment the volume, and add a
Kestrel:Endpoints:Https block in appsettings-org.json that
points at the Let's Encrypt cert files.
The previous section described three separate Dockerfile.runtime*
files, one per runtime service. After the multi-stage refactor
of Dockerfile (6f975f87) and the deletion of those three files
(4fc0ddb6), the section was stale.
Rewrite it to describe the new structure:
- one Dockerfile with multiple stages (build-env, publish-org /
api / blogs, web-runtime / api-runtime / blogs-runtime);
- Dockerfile.backend kept for the production image workflow;
- the BUILD_ENV_TAG ARG that propagates the build-env image
pin across the three locations where it has to be updated.
Update the 'Bumper l'image de build' section to point at the
new ARG location (was: a list of Dockerfile.runtime* files).
Update the isolated-build example to use --target web-runtime
instead of -f Dockerfile.runtime.
Also fix a structural regression introduced while editing: a
duplicate '## Conteneurisation' header and a missing
'## Sessions DDD' transition — both restored here.
After the multi-stage refactor of Dockerfile, the three separate
Dockerfile.runtime* files are obsolete: every runtime image is
now a stage of the main Dockerfile.
Delete Dockerfile.runtime, Dockerfile.runtime.blogs,
Dockerfile.runtime.api, and rewrite docker-compose.yaml so that
each service points at the corresponding target via build.target:
web -> web-runtime (port 5000)
api -> api-runtime (port 5002)
blogs -> blogs-runtime (port 5004)
The build tag is passed through build.args.BUILD_ENV_TAG, which
the Dockerfile declares as an ARG with the same default as
before.
The shared multi-stage Dockerfile is now the single source of
truth for both the build-env image (used by the APK workflow)
and the three runtime images.
Rewrite Dockerfile as a single multi-stage file with seven
stages:
build-env : base image + restore + build + publish
(default target, keeps docker-publish-android
workflow functional — it just needs the APK)
publish-org : isolated copy of Yavsc.Org publish output
publish-api : same for Yavsc.Api
publish-blogs : same for Yavsc.Blogs
web-runtime : ASP.NET image for Yavsc.Org (port 5000)
api-runtime : ASP.NET image for Yavsc.Api (port 5002)
blogs-runtime : ASP.NET image for Yavsc.Blogs (port 5004)
Before this commit, the runtime images tried to COPY from an
external pazof/yavsc-build-env image that did not contain the
published artifacts (only built them, never published). The
multi-stage fix moves the publish step and the runtime copy
into the same Dockerfile, so COPY --from=publish-org etc.
reference local stages — no external artifact coupling.
The build-env tag is now exposed as ARG BUILD_ENV_TAG (default
debian12-dotnet10-android36-v1) so docker-compose can override
it via build.args without editing the Dockerfile.
Add a 'Conteneurisation' section covering:
- the three image families (build env, runtime per project);
- the yavsc-build-env pin on Docker Hub and the
dotnet-android-build-image sibling repo;
- the docker compose up flow (4 services, healthcheck-gated);
- how appsettings-org.json is injected via BuildKit secret mount
(file remains on the host, never lands in a layer);
- the HTTPS-in-prod recipe (uncomment ports 5001/5003/5005 +
/etc/letsencrypt volume + Kestrel:Certificates in
appsettings-org.json + ASPNETCORE_URLS override);
- the bump procedure when the build-env image is rebuilt
(rebuild + push with new tag, then update Dockerfile,
Dockerfile.backend, the three Dockerfile.runtime*, and
docker-compose.yaml in lockstep);
- an isolated build/run check for one runtime image.
Also corrects an outdated mention of 'build.args.BUILD_ENV_IMAGE'
(removed in the previous commit) — the lockstep list now points
at the docker-compose 'build' blocks instead.
'COPY --from=${BUILD_ENV_IMAGE}' is rejected by BuildKit:
failed to solve: failed to parse stage name "${BUILD_ENV_IMAGE}":
invalid reference format: repository name
(library/${BUILD_ENV_IMAGE}) must be lowercase
A COPY --from= can only reference either a local stage of the
same Dockerfile, or a static image reference. ARG interpolation
in the stage name is not supported.
Replace the ARG + interpolation with the pinned tag in all
three Dockerfile.runtime* files. Bumping the build-env image
now means updating Dockerfile, Dockerfile.backend, and the three
Dockerfile.runtime* in lockstep.
Yavsc.Web is an empty template/draft project — no useful code,
conceptually a duplicate of the front that lives in Yavsc.Org.
Remove its mentions from:
- Dockerfile, Dockerfile.backend: the COPY src/Yavsc.Web/*.csproj
step is useless (the project is referenced nowhere downstream)
- ROADMAP.md: the 'Perimetre technique' table no longer lists it
- doc/architecture/decoupage-organisation.md: removed from the
ASCII diagram and the per-project table
Note: this commit only removes references; the empty project
itself (src/Yavsc.Web/) and the yavsc.sln Project() entry are
left in place for now. A future commit can rm -rf the directory
and prune the .sln when we're sure nothing else still depends
on it.
The previous commit referenced ./appsettings-org.json at the repo
root, but the actual file lives at src/Yavsc.Org/appsettings-org.json
(next to its template appsettings-org-template.json).
Without this fix, 'docker compose up' fails with:
failed to stat /home/paul/Workspace/yavsc/appsettings-org.json:
stat ...: no such file or directory
Rewrite docker-compose.yaml to match the runtime architecture
introduced in the previous commits:
- 4 services: db (postgres:16), web (Yavsc.Org), api (Yavsc.Api),
blogs (Yavsc.Blogs). Each runtime service builds from its own
Dockerfile.runtime* with a pinned BUILD_ENV_IMAGE.
- Healthcheck on db via pg_isready. web/api/blogs wait for
service_healthy before starting (was: bare depends_on which
races the DB on cold boot).
- Two named networks: yavsc-internal (db + runtimes) and
yavsc-public (runtimes only). Compose v2 default, but the
split makes the intent explicit and lets the operator
externalise the public network if needed.
- Each runtime service injects appsettings-org.json via the
yavsc_appsettings BuildKit secret (file: ./appsettings-org.json).
No appsettings in the build context.
- HTTPS ports (5001, 5003, 5005) and the /etc/letsencrypt volume
mount are commented out — uncomment them in production when
Kestrel:Certificates is configured in appsettings-org.json.
- The old POSTGRES_* build args on the web service are gone: the
build env no longer needs DB credentials (only the runtime does,
via env_file).
Three new Dockerfiles, each producing a minimal runtime image
based on mcr.microsoft.com/dotnet/aspnet:10.0:
- Dockerfile.runtime : Yavsc.Org (port 5000 HTTP)
- Dockerfile.runtime.blogs : Yavsc.Blogs (port 5004 HTTP)
- Dockerfile.runtime.api : Yavsc.Api (port 5002 HTTP)
Each one:
1. COPY --from=pazof/yavsc-build-env:debian12-dotnet10-android36-v1
/app/publish/<project>/ — i.e. the artifacts produced by the
publish step added in the previous commit.
2. Injects appsettings-org.json via BuildKit secret mount
(--mount=type=secret,id=yavsc_appsettings). The secret never
lands in a layer — BuildKit copies it into /app and discards
the mount.
3. Sets ASPNETCORE_URLS to the project's HTTP port and exposes it.
4. Adds a HEALTHCHECK that pings the root URL.
HTTPS (ports 5001, 5003, 5005) is intentionally NOT exposed in
the Dockerfile: enabling it requires mounting /etc/letsencrypt
(typically via docker-compose) and configuring Kestrel:Certificates
in appsettings-org.json. The compose file in the next commit
documents the volume mount pattern.
Add a 'dotnet publish' step at the end of both Dockerfiles so the
runtime images (Dockerfile.runtime / .blogs / .api) can COPY the
published output via --from=build-env.
Before this commit, the Dockerfiles only ran 'dotnet build' and
ended with CMD ["bash"] — i.e. they were pure build-env images
with no consumable artifact for runtime use.
Dockerfile publishes Yavsc.Org, Yavsc.Api and Yavsc.Blogs (used
by docker-publish-android.yml which extracts the APK).
Dockerfile.backend publishes Yavsc.Org only (used by
docker-publish-backend.yml for the pazof/yavsc production image).
Appsettings are NOT published: they are supplied at runtime via
BuildKit --mount=type=secret (see Dockerfile.runtime in the next
commits) or via a docker-compose volume.
Replace ':latest' with ':debian12-dotnet10-android36-v1' in
both Dockerfiles so the build is reproducible. The build-env
image is constructed from /home/paul/Workspace/dotnet-android-
build-image and pushed to Docker Hub under that tag.
When the build-env image is rebuilt (new dotnet SDK, new
Android SDK, etc.), bump this tag and rebuild the image locally
before re-running the workflows.
The two Dockerfiles serve different GitHub Actions workflows:
- Dockerfile: builds everything including PostIt.Android;
used by .github/workflows/docker-publish-android.yml to
extract the signed APK.
- Dockerfile.backend: builds only Yavsc.Org; used by
.github/workflows/docker-publish-backend.yml to produce the
pazof/yavsc production image.
- 'Decoupage Yavsc.Org vs Yavsc.Server vs Yavsc.Api clarifie
dans l'Architecture'
- 'CONTRIBUTING.md'
Both landed in commits 91bd97c3 / 4ba1aa88 (page) and 7b24b481
(CONTRIBUTING). Update the checkbox status and link to the
artefacts so a reader of ROADMAP.md can navigate directly to
them.
Three items remain in Jalon 0: NuGet centralisation (partial),
containerisation, and BC smoke tests.
When documenting the per-project layout in the previous commit,
I described Yavsc.Blogs as 'Sous-domaine front web specifique
au blog', which is wrong: Yavsc.Blogs contains only ApiController
classes, services and models — no Razor views. The plan is to
deploy it as a headless backend API on a dedicated subdomain in
production, while the blog front (Razor views) stays in
Yavsc.Org to share rendering and auth.
Correct the description, the ASCII diagram, the table row, and
the 'why' paragraph accordingly.
Covers the last missing piece of Jalon 0 in ROADMAP.md:
'CONTRIBUTING.md (build, tests, conventions, DDD sessions)'
Sections:
- prerequisites (.NET 10, PostgreSQL, Node for Avalonia Browser,
Android SDK for PostIt.Android)
- first build + how to start Yavsc.Org in Development
- how to run the test suite
- code conventions (delegated to .editorconfig + a few extras)
- branches and commit messages (trunk-based, scoped imperatives)
- link to architecture/decoupage-organisation.md for the
per-project layout
- DDD sessions (link to doc/ddd-exploration-*.md + ROADMAP.md)
- security reminders (no secrets in git, user-secrets / env vars)
- pointer to GitHub issues + new DDD sessions for design Qs