diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index e85a9b167..140472838 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -27,7 +27,7 @@ jobs: build: runs-on: docker container: - image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v4 + image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v2 steps: - name: Clone yavsc run: | @@ -50,6 +50,9 @@ jobs: echo "🏗️ Building the solution..." cd /src/_src && dotnet build --verbosity normal - name: Test + artifacts: + name: test-results + path: test-results.html run: | echo "🚀 Lancement des tests..." cd /src/_src && dotnet test \ diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index de61bd43d..7d0670a17 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -52,7 +52,7 @@ jobs: release: runs-on: docker container: - image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v4 + image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v2 steps: - name: Clone du repo au tag demandé env: diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 52d500e18..000000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,3 +0,0 @@ -# Conventions du projet - -- Par defaut, mettre a jour [CHANGELOG.md](../CHANGELOG.md) a chaque commit, en respectant sa structure existante et en documentant les changements du commit. Si aucune entree n'est pertinente, expliquer explicitement cette exception avant de committer. diff --git a/.github/workflows/docker-publish-backend.yml b/.github/workflows/docker-publish-backend.yml index 2d84b767f..d8466bdb8 100644 --- a/.github/workflows/docker-publish-backend.yml +++ b/.github/workflows/docker-publish-backend.yml @@ -8,7 +8,8 @@ on: jobs: production-deploy: - runs-on: ubuntu-26.04 + runs-on: ubuntu-latest + steps: # 1. Récupération du code - name: Check out the repo @@ -24,24 +25,8 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 10.0.x - - name: Workloads restore - run: dotnet workload restore - name: Test - env: - TEST_FAILED: "" - run: make test || export TEST_FAILED=$? - - uses: actions/upload-artifact@v7 - with: - name: my-artifact - path: test-reports/**/* - - name: Fail if tests failed - if: ${{ env.TEST_FAILED != '' && env.TEST_FAILED != '0' }} - run: exit $TEST_FAILED - + run: dotnet test --no-build --verbosity normal --filter="Category!=Platform-Android" # 4. Build et Push de l'image de production finale - name: Build and push production image uses: docker/build-push-action@v7 diff --git a/.gitignore b/.gitignore index 57f13079f..32d9c064d 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,3 @@ DataDir/ # Exclude test reports (local test report, not for the repo) /test-reports -/doc/**/.*.md diff --git a/.vscode/settings.json b/.vscode/settings.json index e6f2aee1d..7732a77b1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -46,9 +46,5 @@ } }, "dotnet.defaultSolution": "yavsc.sln", - "axaml.selectedSolution": "/home/paul/Workspace/yavsc/yavsc.sln", - "chat.tools.terminal.autoApprove": { - "dotnet build": true, - "dotnet test": true - } + "axaml.selectedSolution": "/home/paul/Workspace/yavsc/yavsc.sln" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 3320ee74f..011dfe449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,237 +1,13 @@ # Changelog -## [1.0.8-rc25] - unstable - -### Added - -* [Yavsc.Api] Notification du performer à la création d'un rendez-vous - depuis PostIt, selon ses préférences de contact, sans supprimer l'envoi Org. -* [Yavsc.Api][Yavsc.Server] Notification du performer à la signature - client d'un devis (JSON ou image), enregistrée pour l'accueil PostIt - et transmise par e-mail et SignalR. -* [Yavsc.Api] Nouveau point d'entrée `GET /api/v1/notifications/me` - pour exposer les notifications publiques et ciblées à PostIt sur la - Home page ; les notifications d'administration restent filtrées. -* [PostIt] Affichage des notifications sur la page d'accueil, avec - actualisation, indicateur de chargement et message d'erreur. Les - notifications publiques sont accessibles sans connexion et la liste - est rechargée lors d'un changement de session. - -### Changed - -* [PostIt] Insertion des liens de fichiers dès leur ajout au brouillon ; - les liens provisoires des nouveaux billets sont résolus au premier - enregistrement. Save conserve l'édition ouverte et est mis en valeur - lorsque le titre, le corps ou les pièces jointes sont modifiés. - -* [Projet] Instructions Copilot persistantes : mise à jour du changelog - par défaut à chaque commit, avec justification explicite des exceptions. - -### Fixed - -* [PostIt.Tests] Les tests du bandeau de session utilisent le bandeau - réellement monté dans la fenêtre headless, au lieu d'un contrôle détaché. - Les fenêtres sont fermées après chaque test et les quatre boutons - de navigation sont vérifiés. - -* [PostIt] Les liens Markdown des pièces jointes utilisent désormais - le même filtre de nom que l'upload serveur, avant l'encodage URL. - Le libellé conserve le nom original, notamment ses accents. - -* [Yavsc.Abstract][Yavsc.Blogs] Encodage des caractères non supportés - dans les noms de fichiers téléversés avec `_` au lieu de `#`, pour - éviter les fragments d'URL et permettre le téléchargement via les - fichiers statiques, notamment pour les noms accentués. - -* [Yavsc.Api][Yavsc.Blogs] Les appels API et la bande passante sont - désormais enregistrés côté serveur pour les requêtes authentifiées : - le middleware de télémétrie est maintenant branché après - l'authentification, ce qui permet de comptabiliser `ApiCalls` et - `BandwidthMb` au lieu d'enregistrer des zéros. - -## [1.0.8-rc24] - unstable - -### Added - -* [PostIt] Reconnection automatique aux salons de discussion au reconnect - -### Changed - -* [PostIt] La liste des billets et l'éditeur Markdown se partagent - l'espace disponible et défilent séparément ; un séparateur permet - de redimensionner les deux zones. - -### Fixed - -* [PostIt] Défilement des articles longs en édition et en aperçu : - suppression de la limite de hauteur qui masquait le contenu Markdown - et du défilement imbriqué autour d'AvaloniaEdit. Six tests headless - couvrent les barres automatiques, le redimensionnement et le - défilement indépendant de la liste. - -## [1.0.8-rc23] - unstable - -### Added - -* [PostIt] Badge « Publié » à côté du titre des billets publiés dans la - liste, mis à jour lors de la publication ou du retour en brouillon. -* [PostIt] Gestion native du compte inspirée de `/Manage` : modification - du nom d'utilisateur, du nom complet et de l'adresse, téléversement - d'avatar, confirmation e-mail, préférence de courriel mensuel, - création ou changement de mot de passe et activation/désactivation - de l'authentification à deux facteurs. -* [PostIt][Yavsc.Api] Sélection d'un agenda Google, gestion séparée des - comptes bancaires, dissociation des connexions externes et suppression - du compte avec confirmation du nom d'utilisateur. -* [PostIt] Configuration du profil professionnel et des activités, - navigation vers les cercles et les publications depuis le compte. -* [PostIt][Yavsc.Api] Administration des utilisateurs avec recherche - dans les métadonnées du profil et consultation de la consommation - de ressources. -* [Yavsc.Server][Yavsc.Api][Yavsc.Blogs] Suivi de la consommation de - ressources, API de synthèse et migration `ResourceUsageRecords`. -* [Tests] Couverture des parcours profil et professionnel, de l'API de - consommation de ressources et du renouvellement des jetons : - rotation à usage unique, appels simultanés, réponses 401 concurrentes - et rejet d'un jeton de renouvellement révoqué. - -### Changed - -* [PostIt] Réorganisation de la page de compte en sections de profil, - sécurité et préférences ; actions de modification désactivées tant - que le profil n'est pas chargé ou qu'une opération est en cours. -* [PostIt] Utilisation de `PlaceholderText` à la place de `Watermark` - dans les pages de profil et de configuration professionnelle. -* [Yavsc.Api] `GET account/me` renvoie le DTO partagé attendu par - PostIt, avec l'état de sécurité, les connexions externes, les comptes - bancaires et les compteurs du compte. - -### Fixed - -* [Yavsc.Api] Enregistrement des services ASP.NET Identity et des stores - EF nécessaires à `UserManager` : leur absence - empêchait l'activation du contrôleur de compte et provoquait une 500. -* [Yavsc.Api] Chargement du profil : réponse adaptée lorsqu'un jeton - ne contient pas d'identifiant utilisateur ou que le compte n'existe - plus, au lieu d'une exception serveur. -* [PostIt] Synchronisation de tous les chemins de renouvellement des - jetons, y compris après une 401 et au démarrage : les appels - concurrents ne réutilisent plus le même `refresh_token`. Un rejet - `invalid_grant` efface la session et invite à se reconnecter. -* [PostIt] Effacement des informations obsolètes après un échec de - rechargement du profil et normalisation des valeurs de calendrier - lors de la sauvegarde. -* [PostIt][Yavsc.Api] Préservation de l'identité du profil professionnel - lors de son enregistrement. -* [Yavsc.Api] Les coordonnées bancaires ne sont plus modifiées à partir - d'un résumé agrégé : opérations dédiées avec contrôle de propriété - et validation de longueur des champs. - -### Known limitations - -* [PostIt] L'ajout d'un nouveau fournisseur OAuth et la gestion des - crédits ne sont pas encore disponibles dans l'interface native. - -## [1.0.8-rc22] - unstable - -### Fixed - -User experience login into PostIt - -## [1.0.8-rc21] - unstable - -### Fixed - -User experience editing the blog article. - -## [1.0.8-rc20] - unstable - -Chat SignalR fonctionnel de bout en bout (connexion, salons multiples, -envoi/réception de messages), enregistreur audio et localisation dans -PostIt, intégration Live.Avalonia démarrée, et reverse-proxy nginx -apprêté pour WebSocket. - -### Added - -* [PostIt] Interface de chat : page `ChatPage` (connexion au hub, - saisie du nom de salon, liste des messages et des membres présents), - entrée depuis la page d'accueil. -* [PostIt] Envoi de messages dans un salon joint : DTO `ChatMessage` - (Name/Room/Message) pour l'événement `ReceiveMessage`, saisie avec - envoi sur `Enter`, commande inactive tant qu'aucune salle n'est en - focus (le hub refuse l'envoi hors salon, `LabNoJoinNoSend`). -* [PostIt] Modèle multi-salles côté client : `JoinedRooms`, - `SelectedRoom` (salle en focus), messages et utilisateurs indexés par - salle (`FocusedMessages`/`FocusedUsers`), ComboBox de choix de la - salle active ; un `Join` sur une salle déjà jointe ne fait que - changer le focus. -* [PostIt] Enregistreur audio (`AudioRecorder`, interface - `IPCMProcessor`) et ressources de localisation - (`Resources.{resx,en-EN,pt-PT}`). -* [PostIt][WIP] Début d'intégration Live.Avalonia (preview XAML à - chaud ; encore KO). -* [contrib] Bloc `location /chathub` dans `template.vhost` : upgrade - WebSocket (`Upgrade`/`Connection`), `proxy_read_timeout 3600s` et - `proxy_buffering off` — sans quoi la connexion SignalR tombait au - premier silence ou l'upgrade échouait derrière nginx. - -### Changed - -* [PostIt] `StatusNotice` déplacé de `Models` vers `ViewModels` et - enrichi d'une commande d'action optionnelle (`CheckCommand` + - `CheckCommandLabel`, interface `IWithLabel`) : le bandeau de statut - propose désormais l'action contextuelle (Connecter / Déconnecter / - Reconnecter / Vérifier la connexion). -* [PostIt] `ChatViewModel` suit le cycle de vie complet de la - connexion : handlers `Reconnecting` / `Reconnected` / `Closed` de - `WithAutomaticReconnect`, pour refléter l'état réel au lieu d'un - `IsConnected` optimiste. -* [Yavsc.Api] `AddSignalR().AddJsonProtocol(IncludeFields = true)` : - `ChatRoomInfo` n'expose que des champs publics, que System.Text.Json - ne sérialise pas par défaut — le client recevait des objets vides - (`Name` null, `Users` vide) et la liste des salons joints n'était - jamais alimentée. -* [Yavsc.Server] `IConnexionManager.OnConnected` prend le `userName` - en paramètre (résolu depuis les claims dans `ChatHub`) au lieu de le - relire depuis un dictionnaire jamais alimenté. -* [PostIt.Android] `network_security_config.xml` déplacé sous - `Resources/xml/` (chemin canonique Android). - -### Fixed - -* [Yavsc.Server] `KeyNotFoundException` dans - `HubConnectionManager.OnConnected` à chaque connexion SignalR : - `ChatUserNames` (et `ChatCxIds`) n'étaient jamais alimentés — relique - d'un `SetUserName` disparu lors d'une refactorisation. L'exception - dans `OnConnectedAsync` faisait fermer la connexion par le hub, et - PostIt affichait « Déconnecté (The server closed the connection…) » - immédiatement après la connexion. -* [PostIt] URL du hub SignalR : retrait du préfixe `/api/v1` de - `ApiUrl` avant d'apposer `Constants.ChatHubPath` (le hub est mappé à - la racine du service, pas sous le préfixe versionné). -* [PostIt] La `StatusBar` de la page chat référençait `Grid.Row="4"` - sur une grille à 3 rangées (rangée hors limites, bandeau invisible). - -## [1.0.8-rc19] - unstable - -[PostIt] Blogs UX - -### Fixed - -La saisie du billet est maintenant plutôt possible, -en petit écran. - ## [1.0.8-rc18] - unstable -* [PostIt] Page "About" avec lines vers le dépôt et les issues Forgejo - -* Accès aux fichiers personnels PostIt rendu fonctionnel de bout en bout +Accès aux fichiers personnels PostIt rendu fonctionnel de bout en bout (arbre de stockage indexé par le login, pas par le `sub`), boutons d'action des listes réparés (binding `$parent`), revendication `name` enfin présente dans les access tokens (branchement du `ProfileService`), et négociation HTTP/3 côté client PostIt. - ### Added * [PostIt] Négociation HTTP/3 (QUIC) pour les appels API Yavsc : diff --git a/Dockerfile b/Dockerfile index 8ebbb73f0..795b70ab3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,24 +24,41 @@ # ===================================================================== # syntax=docker/dockerfile:1.7 -ARG BUILD_ENV_TAG=debian13-dotnet10-android36-jdk21-v2 +ARG BUILD_ENV_TAG=debian12-dotnet10-android36-v1 FROM pazof/yavsc-build-env:${BUILD_ENV_TAG} AS build-env WORKDIR /src +# (1) Manifest + fichiers de projet pour le cache de restore +COPY *.sln ./ +COPY Directory.*.props ./ +COPY src/Yavsc.Org/*.csproj ./src/Yavsc.Org/ +COPY src/Yavsc.Abstract/*.csproj ./src/Yavsc.Abstract/ +COPY src/Yavsc.Server/*.csproj ./src/Yavsc.Server/ +COPY src/Yavsc.Api/*.csproj ./src/Yavsc.Api/ +COPY src/Yavsc.Blogs/*.csproj ./src/Yavsc.Blogs/ +COPY src/cli/*.csproj ./src/cli/ +COPY src/Yavsc.Org.Tests/*.csproj ./src/Yavsc.Org.Tests/ +COPY src/PostIt/PostIt/*.csproj ./src/PostIt/PostIt/ +COPY src/PostIt/PostIt.Android/*.csproj ./src/PostIt/PostIt.Android/ +COPY src/PostIt/PostIt.Browser/*.csproj ./src/PostIt/PostIt.Browser/ +COPY src/PostIt/PostIt.Desktop/*.csproj ./src/PostIt/PostIt.Desktop/ + +# (2) Tout le code source COPY . . +# (4) Restore RUN dotnet restore -# Build des trois services web principaux +# (5) Build des trois services web principaux RUN dotnet build src/Yavsc.Org/Yavsc.Org.csproj -c Release --no-restore -clp:ErrorsOnly RUN dotnet build src/Yavsc.Api/Yavsc.Api.csproj -c Release --no-restore -clp:ErrorsOnly RUN dotnet build src/Yavsc.Blogs/Yavsc.Blogs.csproj -c Release --no-restore -clp:ErrorsOnly -# Build APK Android (utilisé par le workflow docker-publish-android). +# (6) Build APK Android (utilisé par le workflow docker-publish-android). ARG ANDROID_TARGET_RID=android-arm64 RUN dotnet build src/PostIt/PostIt.Android/PostIt.Android.csproj -c Release --no-restore -clp:ErrorsOnly -r ${ANDROID_TARGET_RID} -# Publish des trois services web (les artifacts sont consommés par +# (7) Publish des trois services web (les artifacts sont consommés par # les stages publish-org / publish-api / publish-blogs ci-dessous). RUN mkdir -p /app/publish RUN dotnet publish src/Yavsc.Org/Yavsc.Org.csproj -c Release --no-build -o /app/publish/Yavsc.Org diff --git a/Dockerfile.backend b/Dockerfile.backend index 46c9a982e..a4e9a54a9 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -1,12 +1,36 @@ # Utilisation de l'image globale pour le build et l'exécution -FROM pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v4 AS build-env +FROM pazof/yavsc-build-env:debian12-dotnet10-android36-v1 AS build-env WORKDIR /src + # 1. Copie de la solution +COPY *.sln ./ +COPY Directory.*.props ./ + +# 2. Copie de l'intégralité des fichiers de projets (.csproj) pour la restauration +COPY src/Yavsc.Org/*.csproj ./src/Yavsc.Org/ +COPY src/Yavsc.Abstract/*.csproj ./src/Yavsc.Abstract/ +COPY src/Yavsc.Server/*.csproj ./src/Yavsc.Server/ +COPY src/Yavsc.Api/*.csproj ./src/Yavsc.Api/ +COPY src/Yavsc.Blogs/*.csproj ./src/Yavsc.Blogs/ +COPY src/cli/*.csproj ./src/cli/ +COPY src/Yavsc.Org.Tests/*.csproj ./src/Yavsc.Org.Tests/ + +# Projets associés à PostIt +COPY src/PostIt/PostIt/*.csproj ./src/PostIt/PostIt/ +COPY src/PostIt/PostIt.Android/*.csproj ./src/PostIt/PostIt.Android/ +COPY src/PostIt/PostIt.Browser/*.csproj ./src/PostIt/PostIt.Browser/ +COPY src/PostIt/PostIt.Desktop/*.csproj ./src/PostIt/PostIt.Desktop/ + +# 4. Copie de l'intégralité du code source COPY . . -RUN dotnet workload restore + # 4. Restauration des dépendances pour tous les projets RUN dotnet restore + +# 5. Copie de la totalité du code source (filtrée par votre .dockerignore) +COPY . . + # 6. Compilation du serveur Web principal RUN dotnet build src/Yavsc.Org/Yavsc.Org.csproj -c Release --no-restore -clp:ErrorsOnly diff --git a/Makefile b/Makefile index 04efba0be..29fd47efa 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,8 @@ DESTDIR=/tmp/yavsc APP_PATH=srv/www/yavsc APP_FULL_PATH=$(DESTDIR)/$(APP_PATH) +include .env + all: dotnet build --nologo @@ -21,7 +23,8 @@ src/Yavsc/bin/output/wwwroot: test: ASPNETCORE_ENVIRONMENT=Development dotnet test --report-xunit \ --report-xunit-html \ - --results-directory "test-reports" + --results-directory "test-reports" \ + --report-xunit-html-filename "test-results.html" test-postit: ASPNETCORE_ENVIRONMENT=Development dotnet test \ diff --git a/README.md b/README.md index 79e711258..fefbdd48d 100644 --- a/README.md +++ b/README.md @@ -232,12 +232,6 @@ L'impact de l'usage d'un nouveau nom d’environnement d'execution, à l'heure d * ~/Views/Home/Index.cshtml * ~/Views/Home/About.cshtml -# 👥 Crédits & Co-auteurs - -Ce projet a été développé de manière hybride : -- **Conception, architecture et revue de code :** Paul Schneider -- **Assistance au code et optimisation :** IA générative (Gemini, Google, Claude (Anthropic), https://qoder.com/ (ex-Windsurf)) - # Remerciements Photo de Dewang Guptasur Unsplash diff --git a/contrib/Makefile b/contrib/Makefile index 5ccf773a1..deb55bcc6 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -14,7 +14,7 @@ reinstall: copy-binaries @echo "💯 backend started" reinstall-prod: - make reinstall APP_INSTALL_PREFIX= APP_INSTALL_ENV=production + make reinstall APP_INSTALL_ENV= APP_INSTALL_PREFIX= APP_INSTALL_ENV=production generated/$(APP_INSTALL_PREFIX)yavscOrg.service: generated/$(APP_INSTALL_PREFIX)yavscBlogs.service: diff --git a/contrib/template.appsettings-blogs.json b/contrib/template.appsettings-blogs.json index 6f8fa02bc..f522feaca 100644 --- a/contrib/template.appsettings-blogs.json +++ b/contrib/template.appsettings-blogs.json @@ -8,7 +8,7 @@ "CorsAllowedOrigins": [ "https://preprod-api.$DOMAIN" ], - "UserFilesRoot": "files" + "Blog": "blog" }, "ConnectionStrings": { "YavscConnection": "Server=localhost;Port=5432;Database=lame-db-name;Username=lame-user-name;Password=lame-password;" diff --git a/contrib/template.appsettings-org.json b/contrib/template.appsettings-org.json index c330f5f32..76203d9e9 100644 --- a/contrib/template.appsettings-org.json +++ b/contrib/template.appsettings-org.json @@ -30,7 +30,7 @@ "EMail": "paul@pschneider.fr" }, "Quota": 200000000, - "UserFilesRoot": "files", + "Blog": "blog", "Avatars": "avatars", "Bills": "bills", "TempDir": "temp", diff --git a/contrib/template.vhost b/contrib/template.vhost index b4c9c139e..ea2c2cb6d 100644 --- a/contrib/template.vhost +++ b/contrib/template.vhost @@ -57,25 +57,5 @@ server { proxy_read_timeout 100s; proxy_buffering off; } - - # SignalR (ChatHub) : WebSocket exige Upgrade/Connection et un timeout - # long, sinon la connexion tombe au premier silence ou l'upgrade échoue. - # Connection est fixé à "upgrade" (pas de map $connection_upgrade, qui - # exigerait une directive map {} globale hors de ce vhost) : SignalR - # négocie toujours un upgrade sur ce chemin, et la négociation initiale - # (POST /negotiate, sans en-tête Upgrade) n'est pas affectée par cet - # en-tête côté Kestrel. - location /chathub { - proxy_pass $BACKEND_SCHEME://$HTTP_HOST:$BACKEND_PORT; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 3600s; - proxy_buffering off; - } include server-status.conf; -} +} \ No newline at end of file diff --git a/doc/architecture/postit.md b/doc/architecture/postit.md index b1bf83cf7..70f3f2fdc 100644 --- a/doc/architecture/postit.md +++ b/doc/architecture/postit.md @@ -126,20 +126,6 @@ le DI est construit. Ordre, dans cet ordre : > à cette table, *ne pas* ajouter de registration pour > `Settings` ailleurs que la ligne `AddSingleton(settings)`. -## Défilement de la page des billets - -La page `BlogsPage` utilise une grille à hauteur contrainte : l'en-tête -et les commandes restent visibles, tandis que la liste des billets et -l'éditeur se partagent l'espace disponible. Un `GridSplitter` permet -de modifier cette répartition. La liste et l'article défilent séparément. - -Dans `MarkdownEditorControl`, AvaloniaEdit gère son propre défilement ; -l'aperçu Markdown est placé dans un `ScrollViewer`. Ne pas limiter -la hauteur du `MarkdownViewer` lui-même : son contenu doit pouvoir -dépasser le viewport pour que les barres automatiques s'activent. -Ne pas placer cette grille dans un `StackPanel` vertical ou un -`ScrollViewer` de page, qui lui donnerait une hauteur de mesure illimitée. - ## Navigation Le host de navigation est un `NavigationPage x:Name="NavRoot"` diff --git a/docker-compose.yaml b/docker-compose.yaml index ea08e9009..f478dcc9f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -26,7 +26,7 @@ services: dockerfile: Dockerfile target: web-runtime args: - BUILD_ENV_TAG: debian13-dotnet10-android36-jdk21-v2 + BUILD_ENV_TAG: debian12-dotnet10-android36-v1 secrets: - source: yavsc_appsettings target: yavsc_appsettings @@ -53,7 +53,9 @@ services: depends_on: db: condition: service_healthy - network_mode: host + networks: + - yavsc-internal + - yavsc-public # volumes: # # Indispensable pour que Yavsc.Org (IdentityServer8) démarre. # # Sans ce montage, 'docker compose up' échoue côté web avec @@ -71,7 +73,7 @@ services: dockerfile: Dockerfile target: api-runtime args: - BUILD_ENV_TAG: debian13-dotnet10-android36-jdk21-v2 + BUILD_ENV_TAG: debian12-dotnet10-android36-v1 secrets: - source: yavsc_appsettings target: yavsc_appsettings @@ -88,7 +90,9 @@ services: depends_on: db: condition: service_healthy - network_mode: host + networks: + - yavsc-internal + - yavsc-public # volumes: # - /etc/letsencrypt:/etc/letsencrypt:ro @@ -101,7 +105,7 @@ services: dockerfile: Dockerfile target: blogs-runtime args: - BUILD_ENV_TAG: debian13-dotnet10-android36-jdk21-v2 + BUILD_ENV_TAG: debian12-dotnet10-android36-v1 secrets: - source: yavsc_appsettings target: yavsc_appsettings @@ -118,7 +122,9 @@ services: depends_on: db: condition: service_healthy - network_mode: host + networks: + - yavsc-internal + - yavsc-public # volumes: # - /etc/letsencrypt:/etc/letsencrypt:ro diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index a353e4b3a..42d09cd55 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -4,7 +4,7 @@ true - 12.1.3 + 12.1.2 @@ -13,21 +13,16 @@ - + - - - - + + - - - @@ -36,4 +31,4 @@ - + \ No newline at end of file diff --git a/src/PostIt/PostIt.Android/MainActivity.cs b/src/PostIt/PostIt.Android/MainActivity.cs index f8fd5a0fd..549100804 100644 --- a/src/PostIt/PostIt.Android/MainActivity.cs +++ b/src/PostIt/PostIt.Android/MainActivity.cs @@ -2,7 +2,6 @@ using Android.App; using Android.Content; using Android.Content.PM; -using Android.Views; using AndroidX.Core.Provider; using AndroidX.Emoji2.Text; using Avalonia.Android; @@ -16,9 +15,7 @@ namespace PostIt.Android; Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, - WindowSoftInputMode = SoftInput.AdjustResize, - ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode) - ] + ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] public class MainActivity : AvaloniaMainActivity { /// diff --git a/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml b/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml index 256555df1..8793aae8a 100644 --- a/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml +++ b/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml @@ -1,10 +1,6 @@  - + diff --git a/src/PostIt/PostIt.Android/Resources/xml/network_security_config.xml b/src/PostIt/PostIt.Android/Resources/xml/network_security_config.xml deleted file mode 100644 index 6d2c97559..000000000 --- a/src/PostIt/PostIt.Android/Resources/xml/network_security_config.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - 10.0.2.2 - - diff --git a/src/PostIt/PostIt.Desktop/Program.cs b/src/PostIt/PostIt.Desktop/Program.cs index 1c5aa0c27..a2b8cd66f 100644 --- a/src/PostIt/PostIt.Desktop/Program.cs +++ b/src/PostIt/PostIt.Desktop/Program.cs @@ -1,7 +1,6 @@ using System; using Avalonia; using PostIt.Services; -using PostIt.CLIOptions; namespace PostIt.Desktop; diff --git a/src/PostIt/PostIt.Tests/BlogApiTestFakes.cs b/src/PostIt/PostIt.Tests/BlogApiTestFakes.cs index 741b3863c..112c11752 100644 --- a/src/PostIt/PostIt.Tests/BlogApiTestFakes.cs +++ b/src/PostIt/PostIt.Tests/BlogApiTestFakes.cs @@ -73,14 +73,6 @@ internal sealed class RecordingYavscApiClient : YavscApiClient public override Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) { _recorder.Calls.Add((method, path, contentFactory)); - if (typeof(T) == typeof(BlogPostDto)) - return Task.FromResult((T)(object)new BlogPostDto - { - Id = 42, - Title = "Mon premier billet", - AuthorId = "tester", - Article = "Contenu du billet de test.", - }); return Task.FromResult(default(T)!); } } diff --git a/src/PostIt/PostIt.Tests/BlogAttachmentLinkTests.cs b/src/PostIt/PostIt.Tests/BlogAttachmentLinkTests.cs index c9d7d3aae..946d3d28e 100644 --- a/src/PostIt/PostIt.Tests/BlogAttachmentLinkTests.cs +++ b/src/PostIt/PostIt.Tests/BlogAttachmentLinkTests.cs @@ -20,13 +20,8 @@ namespace PostIt.Tests; /// public class BlogAttachmentLinkTests { - [Theory] - [InlineData("note.txt", "note.txt")] - [InlineData("INSC_E03686381_SCHNEIDER_000113378AA__Troisi\u00e8me__.PDF", - "INSC_E03686381_SCHNEIDER_000113378AA__Troisi_232me__.PDF")] - [InlineData("rapport final.pdf", "rapport%20final.pdf")] - public async Task Save_with_attachment_appends_absolute_link_derived_from_the_oidc_authority( - string originalName, string urlName) + [Fact] + public async Task Save_with_attachment_appends_absolute_link_derived_from_the_oidc_authority() { const string authority = "https://idp.example.test"; @@ -58,59 +53,27 @@ public class BlogAttachmentLinkTests vm.DraftTitle = "Après"; vm.DraftArticle = "Contenu modifié."; vm.DraftAttachments.Add(new BlogUploadFile( - originalName, + "note.txt", System.Text.Encoding.UTF8.GetBytes("payload test"), "text/plain")); - var expectedUrl = $"{authority}/files/tester/blogs/42/{urlName}"; - Assert.Contains($"- [{originalName}]({expectedUrl})", vm.DraftArticle); - Assert.Empty(recorder.Calls); - var draftBeforeSave = vm.DraftArticle; - await vm.SaveAsync(); - var linkPut = Assert.Single(recorder.Calls, c => c.method == HttpMethod.Put); + // The update branch issues two PUTs: the multipart one + // carrying the file, then — because TryAppendAttachmentLinks + // appended a link — a JSON one carrying the final article. + var linkPut = recorder.Calls.LastOrDefault( + c => c.method == HttpMethod.Put); Assert.NotEqual(default, linkPut); var sent = Assert.IsType>(linkPut.body); var multipartContent = Assert.IsType(sent()); - var sentStringContent = Assert.IsType(multipartContent.First()); + var expectedUrl = $"{authority}/files/tester/blogs/42/note.txt"; + var sentStringContent = multipartContent.First() as StringContent ; var sentString = await sentStringContent.ReadAsStringAsync(TestContext.Current.CancellationToken); - using var document = System.Text.Json.JsonDocument.Parse(sentString); - var article = document.RootElement.GetProperty("article").GetString(); - Assert.Contains($"- [{originalName}]({expectedUrl})", article); + Assert.Contains($"- [note.txt]({expectedUrl})", sentString); // The regression shape: a relative, authority-less link. - Assert.DoesNotContain("](/files/", article); - Assert.Equal(draftBeforeSave, vm.DraftArticle); - Assert.NotNull(vm.SelectedPost); - Assert.False(vm.IsDraftModified); - } - - [Fact] - public async Task New_post_resolves_only_the_provisional_link_at_first_save() - { - var recorder = new CallRecorder(); - var api = new RecordingYavscApiClient(recorder); - var vm = new BlogsViewModel(new BlogApiClient(api, "https://blogs.example.test/api/v1/"), - new Settings { Authentication = new AuthenticationSettings { Authority = "https://idp.example.test" } }); - vm.DraftTitle = "Nouveau billet"; - vm.DraftArticle = "Texte conservé."; - vm.DraftAttachments.Add(new BlogUploadFile("Troisi\u00e8me.PDF", new byte[] { 1 })); - - Assert.Contains("postit-attachment:", vm.DraftArticle); - Assert.Empty(recorder.Calls); - await vm.SaveAsync(); - - Assert.Equal("Nouveau billet", vm.DraftTitle); - Assert.Contains("Texte conservé.", vm.DraftArticle); - Assert.Contains("- [Troisi\u00e8me.PDF](https://idp.example.test/files/tester/blogs/42/Troisi_232me.PDF)", - vm.DraftArticle); - Assert.DoesNotContain("postit-attachment:", vm.DraftArticle); - Assert.Equal(42, vm.SelectedPost?.Id); - Assert.Empty(vm.DraftAttachments); - Assert.False(vm.IsDraftModified); - Assert.Single(recorder.Calls, c => c.method == HttpMethod.Post); - Assert.Single(recorder.Calls, c => c.method == HttpMethod.Put); + Assert.DoesNotContain("](/files/", sentString); } } diff --git a/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs b/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs index 31c82387d..17efd391b 100644 --- a/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs @@ -1,5 +1,4 @@ using System.Net.Http; -using PostIt.Models; using PostIt.ViewModels; using Yavsc; using Yavsc.Api.Client; diff --git a/src/PostIt/PostIt.Tests/HomePageNotificationsTests.cs b/src/PostIt/PostIt.Tests/HomePageNotificationsTests.cs deleted file mode 100644 index 667ea599a..000000000 --- a/src/PostIt/PostIt.Tests/HomePageNotificationsTests.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System.Net.Http; -using Avalonia.Controls; -using Avalonia.Headless.XUnit; -using Avalonia.VisualTree; -using PostIt.Services; -using PostIt.ViewModels; -using PostIt.Views; -using Yavsc.Abstract.Models.Messaging; -using Yavsc.Api.Client; - -namespace PostIt.Tests; - -public class HomePageNotificationsTests -{ - [Fact] - public async Task Refresh_loads_public_notifications_from_the_business_host() - { - var transport = new NotificationTransport(); - var vm = CreateViewModel(transport); - transport.Response = Task.FromResult(new List { Note(1, "Information") }); - - await vm.RefreshNotificationsAsync(); - - Assert.Equal("https://business.example/api/v1/notifications", transport.LastPath); - Assert.True(transport.WasAnonymous); - Assert.Equal("Information", Assert.Single(vm.Notifications).title); - Assert.False(vm.IsLoadingNotifications); - Assert.False(vm.HasNoNotifications); - Assert.Null(vm.NotificationsError); - } - - [Fact] - public async Task Refresh_replaces_notifications_and_distinguishes_empty_from_failed() - { - var transport = new NotificationTransport(); - var vm = CreateViewModel(transport); - vm.Notifications.Add(Note(1, "Old")); - await vm.RefreshNotificationsAsync(); - - Assert.Empty(vm.Notifications); - Assert.True(vm.HasNoNotifications); - - transport.Response = Task.FromException>(new HttpRequestException("Server unavailable")); - await vm.RefreshNotificationsAsync(); - - Assert.Empty(vm.Notifications); - Assert.False(vm.HasNoNotifications); - Assert.Contains("Server unavailable", vm.NotificationsError); - Assert.False(vm.IsLoadingNotifications); - - transport.Response = Task.FromResult(new List { Note(2, "Recovered") }); - await vm.RefreshNotificationsAsync(); - Assert.Null(vm.NotificationsError); - Assert.Equal(2, Assert.Single(vm.Notifications).Id); - } - - [Fact] - public async Task Refresh_ignores_an_older_response_after_a_newer_refresh() - { - var transport = new NotificationTransport(); - var vm = CreateViewModel(transport); - var oldResponse = new TaskCompletionSource>(); - transport.Response = oldResponse.Task; - var oldLoad = vm.RefreshNotificationsAsync(); - Assert.True(vm.IsLoadingNotifications); - - transport.Response = Task.FromResult(new List { Note(2, "Current") }); - await vm.RefreshNotificationsAsync(); - oldResponse.SetResult(new List { Note(1, "Stale") }); - await oldLoad; - - Assert.Equal(2, Assert.Single(vm.Notifications).Id); - Assert.False(vm.IsLoadingNotifications); - } - - [AvaloniaFact] - public async Task Login_and_logout_reload_notifications_and_discard_in_flight_private_data() - { - var tokenPath = Path.Combine(Path.GetTempPath(), $"postit-notifications-{Guid.NewGuid():N}.json"); - var settings = new Settings(); - var store = new TokenStore(tokenPath); - store.Save(new RefreshTokenRecord("test-access-token", "test-refresh-token", - DateTimeOffset.UtcNow.AddHours(1), null)); - await using var api = new YavscApiClient(settings, store); - try - { - var session = new SessionStatusViewModel { Api = api }; - var transport = new NotificationTransport(); - var vm = CreateViewModel(transport, session); - session.Refresh(); - Assert.False(transport.WasAnonymous); - Assert.EndsWith("/notifications/me", transport.LastPath); - var privateResponse = new TaskCompletionSource>(); - transport.Response = privateResponse.Task; - var privateLoad = vm.RefreshNotificationsAsync(); - Assert.False(transport.WasAnonymous); - Assert.EndsWith("/notifications/me", transport.LastPath); - - vm.Notifications.Add(Note(9, "Private")); - transport.Response = Task.FromResult(new List { Note(2, "Public") }); - session.SetError("Disconnected"); - Assert.Equal(2, Assert.Single(vm.Notifications).Id); - Assert.True(transport.WasAnonymous); - - privateResponse.SetResult(new List { Note(1, "Late private data") }); - await privateLoad; - Assert.Equal(2, Assert.Single(vm.Notifications).Id); - } - finally - { - File.Delete(tokenPath); - } - } - - [AvaloniaFact] - public async Task Home_page_loads_on_navigation_and_renders_wrapped_titles_and_bodies() - { - var transport = new NotificationTransport - { - Response = Task.FromResult(new List { Note(1, "Notification title") }) - }; - var vm = CreateViewModel(transport); - var page = new HomePage { DataContext = vm }; - var nav = new NavigationPage(); - var window = new Window { Content = nav, Width = 320, Height = 450 }; - try - { - window.Show(); - await nav.PushAsync(page); - window.UpdateLayout(); - - Assert.Single(vm.Notifications); - var texts = page.GetVisualDescendants().OfType().ToArray(); - Assert.Contains(texts, t => t.Text == "Notification title" - && t.TextWrapping == Avalonia.Media.TextWrapping.Wrap); - Assert.Contains(texts, t => t.Text == "Notification body" - && t.TextWrapping == Avalonia.Media.TextWrapping.Wrap); - var refresh = Assert.Single(page.GetVisualDescendants().OfType