Espace de stockage personnel géré depuis PostIt, avec attachement par
référence (sans duplication) des fichiers à la demande du client
(NominativeServiceCommand) et au devis du fournisseur (Estimate).
Backend:
- UploadedFile.OwnerId (FK ApplicationUser, nullable) ; les téléversements
vers api/v1/fs créent/mettent à jour une ligne propriétaire.
- Tables de jointure EstimateAttachedFile et QueryAttachedFile (PK composite),
migration UserFileAttachments.
- EstimateApiController: GET/POST/DELETE estimate/{id}/attachments
(fournisseur uniquement, fichier devant appartenir au caller).
- FrontOfficeApiController: GET/POST/DELETE front/query/{id}/attachments
(client uniquement).
- FileSystemApiController: GET fs/file/{fileId} sert les octets depuis le
host Blogs (autorisation propriétaire ou partie au devis/demande via DB
partagée) — le host API ne gère que les métadonnées, contrainte des
conteneurs docker à disques distincts.
Client .NET:
- UserFilesApiClient (list/upload/delete/download) pointant vers BlogsApiUrl.
- DTOs UserDirectoryDto/FileEntryDto/AttachmentDto/FileReceivedDto/UploadFile.
- Méthodes d'attachement sur EstimateApiClient et FrontOfficeApiClient.
PostIt (Avalonia):
- Page « Mes fichiers » (naviguer/téléverser/télécharger/supprimer), servant
aussi de sélecteur pour l'attachement.
- Section pièces jointes dans l'édition du devis (fournisseur) et le détail
de la demande (client), avec ajout depuis My Files, détachement,
téléchargement.
- Enregistrement DI + mapping ViewLocator + bouton d'accès.
Tests:
- EstimateEditionPageViewModelTests mis à jour (nouveau ctor fsClient,
historique d'appels du stub pour les assertions post-envoi).
Nommage HTTP/méthodes en anglais, documentation en français.
Co-Authored-By: Claude Code <noreply@anthropic.com>
- List removal: GetOngoingEstimatesAsProvider filters on
ProviderValidationDate so a provider's signed estimate leaves their
list; PostIt refreshes the list on navigation pop-back.
- Title renames: "Devis en attente de signature fournisseur/client"
on both the provider and client sides (HomePage, VM, tests).
- Move the estimate/bill TeX+PDF generation flow from Yavsc.Org into
Yavsc.Api (TeXHelpers, PdfGenerationViewModel, BillViewComponent,
Bill/Estimate templates, _ViewImports). Program.cs wires
AddControllersWithViews + SiteSettings; FrontOfficeApiController
injects IRazorViewEngine (cast to IViewEngine) with optional deps
and exposes estimate.tex/estimate.pdf endpoints keyed by query id.
- PDF generation drives through the Estimate_pdf view (side-effect
render) and compiles via the system lualatex, fed TeX on stdin (no
intermediate .tex), PDF to the bills dir, aux artifacts cleaned so
only the .pdf persists. Resolve the bills dir to an absolute path so
lualatex's -output-directory and working directory agree regardless
of the host cwd (fixes "I can't write on file estimate-1.log").
- PostIt: DownloadAsync on the client + GetEstimateTex/PdfAsync;
Télécharger PDF/TeX buttons on the validation and command-detail
pages via a StorageProvider save helper.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Reopening an already-validated estimate showed an empty pad: the
strokes were stored server-side but never sent back. Embed them in the
estimate payload and load them straight into the pad, in the same wire
format PostIt captured — no separate fetch.
- Server: Signature inverse navs get STJ [JsonIgnore] (cycle-safe
under System.Text.Json, matching ApplicationUser); the four estimate
read endpoints Include(e => e.Signatures) so strokes ride the payload
- Client: EstimateDto.Signatures with EstimateSignatureDto (Strokes,
Type=Pro/0|Client/1, CoordinateMax, CapturedAtUtc)
- PostIt: SignaturePadControl.Load(int[]) replaces the buffer and
raises RedrawRequested; EstimateValidationPageViewModel.LoadExisting
Signature() picks the perspective's side off the estimate and loads
it on Attach
- Tests: server asserts GET estimate/{id} carries the strokes; PostIt
VM tests cover load + side-by-perspective
Co-Authored-By: Claude Code <noreply@anthropic.com>
Adds the client-side ability to open an estimate from the list, draw a
signature on a reusable SignaturePadView control, and accept or reject it
against /api/v1/front/query/accept|reject. The server resolves the actor
role (provider vs client) from the caller's identity, so the same page works
for either party.
- Yavsc.Api.Client: QueryAcceptanceDto + FrontOfficeApiClient (accept/reject)
- PostIt: extract SignaturePadView UserControl from SignaturePage; new
EstimateValidationPage (VM + view) with Valider/Refuser/Effacer/Retour;
wire OpenEstimate entry point from EstimateListPage; register client/page
in DI + ViewLocator
- Tests: FrontOfficeApiClient + EstimateValidationPageViewModel unit tests;
update EstimateListPageViewModelTests ctor
Co-Authored-By: Claude Code <noreply@anthropic.com>
Round-trip out a long-standing hole in the ACL feature: until
this commit a circle on Yavsc was an empty named bucket. You
could create 'Famille' and grant it on a post, but the circle
carried no members, so 'Famille' authorised no one. The MVC
admin controller (Yavsc.Org.Controllers.CircleMembersController)
existed but had no REST counterpart, so PostIt — which only
talks to the Yavsc.Blogs API — had no way to manage membership
at all.
This commit closes that gap end-to-end:
Server (Yavsc.Blogs)
- GET /api/circle/{id}/members list members
- POST /api/circle/{id}/members add a user (body { userId })
- DELETE /api/circle/{id}/members/{userId} remove a user
All three are scoped to caller == circle.OwnerId; non-owned
circles return 404 (not 403) to avoid leaking existence, in
line with the rest of the controller.
- Two new DTOs (CircleMemberDto, AddCircleMemberDto) for the
wire shapes. CircleMemberDto mirrors UserSearchResultDto
minus Email — membership UI doesn't need contact details.
Tests (Yavsc.Blogs.Tests)
- CircleMembersApiTests: 5 [Fact] covering empty list,
add+get, duplicate add → 409, remove, and cross-owner 404.
Test users (alice, bob) are seeded directly through the
in-memory DbContext — the Blogs fixture doesn't stand up
UserManager<ApplicationUser>.
Client (Yavsc.Api.Client)
- CircleMemberDto + 3 methods on CircleApiClient:
GetMembersAsync, AddMemberAsync, RemoveMemberAsync.
All match the server's contract: 404 flattens to null,
409 surfaces as an exception (callers can dedupe beforehand
if they want idempotent behaviour).
UI (PostIt)
- CirclesPageViewModel gains a Members ObservableCollection
that auto-loads on SelectedCircle change (via the partial
setter generated by [ObservableProperty]). Commands:
LoadMembersAsync, OpenAddMember (raises an event the view
subscribes to), OnAddMemberConfirmedAsync (called by the
view when the dialog confirms a selection), RemoveMemberAsync.
409 (already a member) is detected from the exception
message and surfaced as a friendly status rather than an
error — a likely race when the same user gets added twice
through two UI paths.
- CirclesPage layout is now two-pane (circles + editor on the
left, members of the selected circle on the right). The
member pane has an 'Ajouter un membre' button that opens
AddCircleMemberDialog. Code-behind wires the dialog's
Confirmed event back into the VM via an async lambda
wrapper (EventHandler<T> wants void, the VM method is
async Task).
- AddCircleMemberDialog is a ContentPage (light modal, same
pattern as PostAclDialog). Its ViewModel consumes
IUserDirectory — the abstraction introduced by af5e71aa
to fix the 'user search should not be IContactService'
confusion. The dialog raises Confirmed with the picked
UserSummary; the host (CirclesPage) is responsible for
calling CircleApiClient.AddMemberAsync.
Out of scope (tracked in MEMORY.md, 2026-08-18):
- i18n: all visible text still hard-coded French.
- XAML accessibility audit of pre-existing pages.
- Avalonia.Headless UI tests of the new navigation flow.
Tests: 51/51 PostIt.Tests green, 20/20 Yavsc.Blogs.Tests
green (was 15; +5 for CircleMembersApiTests), 44/44
Yavsc.Org.Tests green (no regression).
Adds the client-side half of the user-search endpoint landed
on the server in ac9eb2f0 (commit 6 on this branch). The
client mirrors the server's filter contract:
- query: substring match on FullName or UserName
- email: exact match on Email
- take: 1..100, default 25
Empty (query + email) short-circuits to an empty list
client-side rather than letting the server return the first
`take` users alphabetically — the address-book UX is
"type to search", not "show me a directory".
The DTO (Yavsc.Api.Client.Dtos.UserSearchResultDto) is a flat
shape (Id, UserName, FullName, Avatar, Email) with no
navigation properties; field names match the JSON the server
emits so deserialisation is a no-op.
PostIt wiring:
- App.axaml.cs constructs a UserSearchClient singleton and
registers it alongside CircleApiClient and BlogAclApiClient.
- The PostIt.csproj ProjectReference to Yavsc.Api.Client was
in place before this commit on feat/postit-acl; the rebase
of feat/app-invite on top of feat/postit-acl dropped it.
This commit re-adds it.
Creates the high-level HTTP client library the PostIt UI will
consume to manage blog posts, circles, and per-post ACLs.
Clients in this commit:
- BlogApiClient (moved from PostIt/Services; same public surface,
now depends on IYavscApiClient instead of the concrete class).
- CircleApiClient (new): GET/POST/PUT/DELETE /api/circle. Takes
the blogs base URL explicitly in its constructor so it doesn't
need to know about PostIt's Settings type.
- BlogAclApiClient (new): GET/POST/PUT/DELETE /api/blogacl.
Same conventions as CircleApiClient.
DTOs (Yavsc.Api.Client.Dtos):
- CircleDto: id, name, ownerId, public. Stops short of the
navigation properties on the server-side Circle (Owner,
Members), which depend on ApplicationUser and other server
types we don't want to drag into the client.
- CircleAuthorizationDto: circleId, blogPostId, comment. Same
reason: the server entity has Target and Allowed navigation
properties the client never needs.
The clients now require the caller to pass the blogs base URL
explicitly in the constructor (previously the BlogApiClient
sniffed it off YavscApiClient.Settings.BlogsApiUrl, but that
field is PostIt-specific). The one production call site
(App.axaml.cs) and four test call sites are updated to pass
the URL.
Build + 51/51 tests green. The IYavscApiClient abstraction was
landed in the previous commit so this one could be a pure
addition + relocation.