Architecture de SOFTIC Intranet
هذا المحتوى غير متوفر بعد باللغة العربية؛ يُعرض باللغة Français.
1. Vue d'ensemble
Navigateurs (web, PWA) Application mobile (lot 3) Produits SOFTIC
circuit Blazor Server REST + SignalR GED, GMAO, Tender, WRDPA…
│ WebSocket /_blazor │ /api/v1 /hubs/realtime │ /api/integration/v1 (X-Api-Key)
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ Softic.Intranet.Web — UN SEUL PROCESSUS (IIS in-process) │
│ Shell + modules Blazor ─────┐ Softic.Intranet.Api (contrôleurs, hub, relais) │
│ ▼ │ │
│ Softic.Intranet.Engine ◄─────────────┘ │
│ services métier sans état ──publient──► IRealtimeBus (mémoire) │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Softic.Intranet.Data (EF Core) circuits Blazor relais SignalR │
└───────────────────────────────┬─────────────────────────────────────────────────────────────┘
▼
SQL Server : Softic_Intranet
Pourquoi un seul processus ? Le temps réel est l'exigence n° 1. Avec l'interface, l'API et le hub dans le même processus, un simple bus en mémoire suffit : pas de backplane Redis, pas de file de messages, aucune latence. Une action (web, mobile ou produit externe) → le service métier enregistre → publie sur le bus → tous les écrans concernés se redessinent dans la même milliseconde.
2. Le cadre « application de bureau »
| Élément | Fichier | Rôle |
|---|---|---|
| Barre de titre | Shell/TitleBar.razor |
logo, recherche Ctrl+K, société, cloche, menu utilisateur |
| Volet de navigation | Shell/NavPane.razor |
modules selon les permissions, pastilles vivantes, repliable |
| Onglets | Shell/TabStrip.razor + Services/WorkspaceService.cs |
un onglet par module, état conservé, clic molette, menu contextuel |
| Barre d'état | Shell/StatusBar.razor |
liaison temps réel, société, personnes en ligne, heure de la société |
| Palette de commandes | Shell/CommandPalette.razor |
modules, personnes, tâches, actualités, applications |
| Notifications | Shell/NotificationPanel.razor, Shell/ToastHost.razor |
panneau latéral, toasts, son, notifications du système |
Onglets conservés en mémoire. Une seule page Razor (Workspace.razor, route /{*Path}) reçoit toutes les adresses.
MainLayout résout l'adresse en module (ModuleCatalog.Resolve) et l'ouvre dans un onglet. Tous les onglets ouverts
restent instanciés (masqués en CSS) : filtres, saisie en cours et position de défilement survivent au changement d'onglet.
Chaque onglet est isolé dans un ErrorBoundary : une erreur ne fait tomber que lui.
Ajouter un module = une ligne dans Services/ModuleCatalog.cs + un composant héritant de ModulePageBase.
Navigation, onglets, palette et liens profonds suivent automatiquement.
Raccourcis : Ctrl+K recherche · Alt+W fermer l'onglet · Alt+N notifications · Échap fermer.
3. Temps réel
IRealtimeBus(Engine/Realtime/RealtimeBus.cs) :Publish(RealtimeMessage)/Subscribe(...).- Un message cible : des utilisateurs nommés (
UserIds), une société (CompanyId), les détenteurs d'une permission dans la société (Permission), ou tout le groupe. Une seule règle de routage :RealtimeBus.IsRecipient, partagée par le web et le mobile. - Web :
UserSession(service par circuit) s'abonne, met à jour les compteurs, relaie aux composants ;ModulePageBase.Listen(...)+OnRealtimeAsyncredessinent dans le bon contexte. - Mobile :
RealtimeHubRelaypousse les mêmes événements vers/hubs/realtime(méthode clienteevent). - Présence :
PresenceTracker— en ligne tant qu'il reste une connexion active ; liaison coupée = « absent du réseau ». - Reconnexion : après un recyclage du pool IIS ou une mise à jour,
wwwroot/js/intranet.jsattend que/healthréponde puis recharge la page tout seul.
Sujets : notification.created|changed, task.changed|deleted, news.changed, presence.changed,
directory.changed, apps.changed, session.revoked ; lot 2 : chat.message, chat.conversation, chat.read,
chat.typing, calendar.changed, space.changed.
4. Multi-sociétés
Company(hiérarchie, fuseau IANA, langue, logo) →OrgUnit(arbre) →UserCompany(un rôle par société).UserContext= un utilisateur dans une société, avec les permissions de son rôle dans cette société. Il est passé explicitement à tous les services : aucun état ambiant, aucun filtre global caché.- Changer de société = réémission du cookie (ou du jeton mobile) puis rechargement du poste de travail.
- Super-administrateur : entre dans toutes les sociétés avec toutes les permissions. Il en reste toujours un.
- Actualités : par société ou « tout le groupe ». Notifications : par utilisateur, toutes sociétés confondues.
- Dates : stockées en UTC, affichées dans le fuseau de la société de travail (Tunis, Luanda…).
5. Sécurité
| Sujet | Choix |
|---|---|
| Mots de passe | PBKDF2-HMAC-SHA512, 210 000 itérations, sel aléatoire ; verrouillage après N échecs (paramétrable) |
| Web | cookie softic_intranet (HttpOnly, SameSite Lax), revalidé à chaque requête et toutes les 2 min dans le circuit |
| Mobile | JWT HS256 court + jeton de rafraîchissement haché, en rotation ; rejeu d'un ancien jeton = toutes les sessions mobiles fermées |
| Produits | clé sk_préfixe_secret : seule l'empreinte SHA-256 est stockée, comparaison à temps constant |
| Secrets en base | chiffrés par ASP.NET Core Data Protection ({Storage:Root}\keys, DPAPI machine sous Windows) |
| API | Bearer uniquement (jamais le cookie) → pas de CSRF ; limitation de débit sur les points d'entrée d'authentification |
| Sessions | « Fermer les sessions » change le tampon de sécurité : cookies, circuits ouverts et jetons tombent immédiatement |
| Audit | connexions, administration, lancements d'applications, appels d'intégration |
6. Traductions (fr, en, ar, pt)
- Aucune chaîne en dur : tout passe par
Softic.Intranet.Shared/Resources/SharedResource*.resx(@L["Clé"]dans l'interface,Texts.Get(clé, langue)côté serveur). - Les notifications sont rédigées dans la langue du destinataire, pas de celui qui agit.
- Arabe :
dir="rtl"posé par_Host.cshtml; les feuilles de style n'utilisent que des propriétés logiques. - Les erreurs métier sont des clés (
BusinessException("Error.LoginTaken")) traduites par l'interface ou l'API. - Les textes fournis par DevExpress lui-même (pagination des grilles…) restent en anglais dans ce lot.
7. Paramètres
Tout se règle dans Administration → Paramètres (effet immédiat). Seuls restent en fichier :
la chaîne de connexion, Storage:Root et le mot de passe d'amorçage (appsettings.Production.json, jamais publié).
8. Lot 2 — messagerie, agenda, réunions, espaces d'équipe, fichiers
Même découpage que le socle : entités dans Domain/Entities/Communication.cs, services sans état dans Engine
(Chat, Calendar, Meetings, Spaces, Files), contrôleurs dans Api, modules dans Web/Modules.
Aucune nouvelle permission : les salles se gèrent avec admin.companies ; tout le reste découle de l'appartenance
(à une conversation, à un espace, à la liste des invités).
Messagerie (/chat)
- Trois sortes de conversation : directe (clé unique
petitId:grandId+ index unique = un seul fil par paire, même si les deux personnes cliquent en même temps), groupe (propriétaire, renommage, ajout/retrait, quitter avec transmission du rôle au plus ancien membre), espace (membres alignés sur ceux de l'espace). - Transverse au groupe : un collaborateur de Tunis écrit à un collaborateur de Luanda.
- Non-lus :
ConversationMember.LastReadMessageIdsuffit aux pastilles ET aux accusés de lecture ; la messagerie ne crée pas de notification persistante (le centre de notifications reste lisible). Hors du fil affiché : toast + son, sauf conversation en sourdine ou statut « ne pas déranger ». - « écrit… » est un événement éphémère, jamais stocké. Message supprimé = texte effacé + pièce jointe supprimée du disque.
- Interface :
ChatThread.razorest réutilisable (module Messagerie et onglet Discussion d'un espace). Le champ de saisie est piloté parwwwroot/js/intranet.js(bindComposer: Entrée envoie, Maj+Entrée va à la ligne). Chaque fil affiché inscrit une « sonde » dansUserSession: le cadre sait ainsi si un message a besoin d'un toast.
Agenda (/calendar)
- Dates en UTC. Une série récurrente est stockée une fois puis développée à la lecture par
OccurrenceExpander(fonction pure, testée) dans le fuseau de la société : une réunion de 9 h 30 reste à 9 h 30 été comme hiver. Modifier = toute la série ; supprimer une occurrence = une ligneEventSkip. - Invités de tout le groupe, réponses (accepté / peut-être / refusé) ; changer l'horaire remet les réponses en attente et prévient les invités. Pastille du module = invitations sans réponse.
- Salles et ressources (
/admin/rooms) : détection de conflit à l'enregistrement, séries comprises (180 jours). - Confidentialité : un événement privé d'autrui n'est qu'un créneau « Occupé » — le moteur ne renvoie aucun détail, l'interface ne peut donc pas en fuiter.
- Rappels :
ReminderWorker(chaque minute) + journalEventReminderLog= un seul rappel par occurrence, même après un redémarrage. - Vues jour / semaine / mois / liste écrites en HTML/CSS (
CalendarPage.razor) : glisser-déposer, superposition d'agendas de collègues et de salles, bande de disponibilités dans la fiche. Les formulaires utilisent les éditeurs DevExpress déjà employés au lot 1.DxSchedulern'est volontairement pas utilisé dans ce lot (voir FEUILLE-DE-ROUTE).
Réunions en un clic (WMeet / WMeet)
- Une réunion = un nom de salle impossible à deviner (
softic-{société}-{12 caractères}) gardé dans l'événement ou le message. L'adresse est construite au moment du clic, pour la personne qui entre, parMeetingLinkService:- mode simple : gabarit
{baseUrl}/{room}(jokers{name},{email},{subject}) ; - mode jeton : si
Meetings.JwtAppSecretest renseigné, jeton JWT WMeet standard (HS256) :iss= app_id,aud,sub= domaine,room,context.user(8 heures).
- mode simple : gabarit
- Côté web, « Rejoindre » est un vrai lien
/meet/{salle}ouvert dans un nouvel onglet (page Razor qui redirige) : aucun blocage de fenêtre surgissante, et la caméra / le micro ne sont pas bridés par l'en-têtePermissions-Policyde l'intranet comme ils le seraient dans un cadre intégré. - Étape suivante possible (non codée, faute de contrat) : si WMeet expose une API « créer une réunion »
(
POSTauthentifié parX-Api-Keyrendant l'adresse et l'identifiant), il suffira de l'appeler dansMeetingLinkServicepour retrouver comptes rendus et indice TAHLIGA rattachés à l'événement de l'intranet.
Espaces d'équipe (/spaces)
- Un espace = des membres (de toutes les sociétés), un fil de discussion (conversation synchronisée par
ChatService.SyncSpaceConversationAsync), des tâches partagées (TaskItem.SpaceId), des fichiers, des événements. - Privé = visible de ses seuls membres ; ouvert = découvrable et rejoignable dans sa société. Archivé = lecture seule.
- Gestion : propriétaires de l'espace, ou administrateur de la société (
admin.companies).
Fichiers
- Contenu sur disque :
{Storage:Root}\files\aaaa\MM\{guid}.bin— le nom d'origine n'est jamais un chemin ; la base ne garde que la fiche. À sauvegarder avec la base. - Droit de lecture = appartenance à la conversation ou à l'espace (sinon
404, pas403: on ne confirme pas l'existence). - Web :
GET /files/{id}/{nom}(cookie). Seuls images et PDF s'affichent dans le navigateur ; le reste est proposé en téléchargement, avecX-Content-Type-Options: nosniffet un bac à sable CSP. Extensions interdites et taille maximale paramétrables.
9. Lot 3 — Softic IA partout, coûts et revente ; mode intégré des applications
Le principe commercial, inscrit dans le moteur
SOFTIC paie le fournisseur d'IA et refacture la consommation à chaque société avec sa marge. Tout appel à l'IA,
d'où qu'il vienne (assistant, messagerie, tâches, agenda, actualités, espaces, API mobile), passe par un seul service,
Engine/Ai/AiService, qui l'autorise, le borne, le compte, le valorise et le journalise :
écran / API ──► AiService.EstimateAsync ──► fourchette du prix de vente AVANT exécution (+ confirmation au-delà du seuil)
──► AiService.CompleteAsync
1. autorisation : IA activée · clé présente · permission ai.use · société activée · budgets du mois
2. choix du modèle par GAMME (Rapide / Standard / Expert) — la fonction appelante décide, pas l'utilisateur seul
3. IAiProvider.CompleteAsync ── flux SSE ──► texte affiché au fil de l'eau
4. jetons annoncés par le fournisseur ──► coût fournisseur ──► prix de vente ──► ligne AiUsages
5. sujet temps réel ai.usage ──► jauges et tableau de bord à jour
| Notion | Règle |
|---|---|
| Coût fournisseur | jetons entrants × prix entrée + jetons sortants × prix sortie (prix en $ par million de jetons, table AiModels) |
| Prix de vente | coût × taux de change × (1 + marge %), arrondi au-dessus : jamais vendu sous le prix visé |
| Montants | entiers en millionièmes de devise (…Micro) : aucune erreur d'arrondi, sommes exactes sur tous les moteurs de base |
| Marge | paramètre général Ai.MarginPercent, ou marge propre à une société (AiCompanyPlans) |
| Budgets | par société et par utilisateur, au prix de vente, par mois civil (UTC) ; atteint = IA en pause, rien n'est facturé au-delà |
| Échecs | journalisés sans montant (panne, quota du fournisseur…) : ils expliquent un trou dans les statistiques |
| Figé à l'appel | chaque ligne garde sa marge, son taux de change et sa devise : modifier un tarif ne réécrit jamais l'historique |
Cloisonnement (couvert par un test) : un utilisateur et l'administrateur d'une société ne voient que le prix de vente ; le coût fournisseur, la marge réalisée, les tarifs et l'activation par société sont réservés au super-administrateur, c'est-à-dire SOFTIC. Le relevé CSV suit la même règle (colonnes de coût pour SOFTIC seulement).
Marque blanche : l'interface, les messages et la consigne donnée au modèle disent « Softic IA ». Le nom du
fournisseur n'existe que dans les paramètres Ai.Provider.* et dans les identifiants techniques de la table AiModels
(amorcés par DbSeeder.SeedAiAsync, modifiables dans l'application). Le libellé d'un modèle (« Softic IA · Standard »)
est le seul nom montré aux utilisateurs.
Fournisseur remplaçable : le moteur ne connaît que l'interface IAiProvider. MessagesApiProvider parle l'API
« Messages » en flux d'événements ; un autre fournisseur (ou un modèle local) s'ajoute par une classe et une ligne
d'enregistrement (TryAddSingleton<IAiProvider, …> avant AddIntranetEngine), sans toucher aux coûts ni aux écrans.
« L'IA partout » : deux composants, un catalogue
| Élément | Rôle |
|---|---|
Modules/Ai/AiPanel.razor |
l'assistant : panneau latéral ouvert depuis la barre de titre (✨) sur tous les écrans ; conversation en direct, prix de chaque réponse, gamme, consommation du mois, avertissement « à vérifier » |
Modules/Ai/AiAction.razor |
le bouton d'IA d'un écran : l'écran fournit la demande (BuildRequest) et reçoit le texte accepté (OnAccept) ; le composant gère estimation, confirmation, flux, prix réel, copie, relance |
Engine/Ai/AiPrompts.cs |
catalogue des fonctions : clé de suivi des coûts, gamme, consigne, longueur maximale |
Fonctions livrées : résumer une conversation et proposer une réponse (messagerie, gamme Rapide), rédiger la description
d'une tâche, proposer un ordre du jour (agenda), améliorer et traduire une actualité (fr, en, ar, pt), synthèse d'un
espace d'équipe. Ajouter l'IA à un écran = une méthode dans AiPrompts + une balise <AiAction …>.
L'IA propose, l'utilisateur dispose : aucun texte n'est envoyé ni enregistré sans son accord (la réponse proposée
arrive dans la zone de saisie, la description remplace le texte seulement après « Remplacer le texte »).
Données envoyées au fournisseur : uniquement ce que la fonction rassemble sous les yeux de l'utilisateur (les 60 derniers messages du fil ouvert, le titre de la tâche, le texte de l'actualité…), jamais une base entière. La consigne interdit d'inventer noms, chiffres, dates ou engagements. Les conversations de l'assistant ne sont pas conservées (seul le journal de consommation l'est).
Mode intégré des applications : un seul en-tête
Quand un produit s'ouvre dans un onglet de l'intranet, le volet de navigation s'efface (is-nav-hidden, réglage
Workspace.AutoHideNavForApps) et le produit masque son propre en-tête : langue, affichage et authentification sont
ceux de l'intranet. Le ticket SSO porte culture, theme, embedded, intranet ; un pont postMessage restreint aux
deux origines déclarées transporte le thème en direct, la déconnexion et le titre de la page. Côté produit, le projet
Softic.Intranet.Client.AspNetCore fait le travail en trois appels — voir INTEGRATION-PRODUITS.md § 4.
10. Ce qui a été vérifié avant livraison
- compilation complète de la solution (0 erreur, 0 avertissement) contre le vrai EF Core 8 ;
- modèle validé avec le fournisseur SQL Server : 21 tables, aucune cascade multiple ;
- exécution réelle sur SQLite : connexion, rendu de tous les modules, 4 langues, contrôle d'accès, API mobile, API d'intégration ;
- 17 tests automatisés (sécurité, routage temps réel, multi-sociétés, tâches, actualités, jetons, intégration, traductions) ;
- DevExpress n'étant pas disponible hors de votre poste, ses composants ont été remplacés par des bouchons : 907 attributs ont été contrôlés contre la liste des paramètres 24.1 utilisés, mais c'est le point à surveiller à la première compilation.
Lot 2, mêmes moyens :
- compilation complète à 0 erreur / 0 avertissement ; modèle SQL Server : 32 tables, 17 cascades, aucun chemin multiple ;
- 26 tests automatisés (9 nouveaux : fil direct unique, groupe et rôles, réunion / jeton WMeet, récurrence et changement d'heure, invitations / salle / confidentialité / série, rappel unique, espace d'équipe, fichiers, cohérence des données de démonstration) ;
- exécution réelle sur SQLite : rendu des nouveaux écrans (4 vues de l'agenda, 4 langues, RTL), et scénario complet de l'API : messagerie, pièces jointes, droits, agenda, conflit de salle, réponses, occurrences, réunions, espaces, recherche ;
- 1 355 attributs DevExpress contrôlés — aucun nouveau composant DevExpress n'a été introduit dans ce lot ;
- non vérifiable hors navigateur : le comportement interactif (glisser-déposer de l'agenda, compositeur de message,
dépôt de fichiers par
InputFile, défilement du fil). C'est le scénario de recette dePROCEDURE-MISE-A-JOUR.md.
Lot 3, mêmes moyens :
- compilation complète des 9 projets à 0 erreur / 0 avertissement ; modèle SQL Server : 35 tables, aucun chemin de cascade multiple ;
- 31 tests automatisés (5 nouveaux : calcul du coût et du prix de vente, IA éteinte → activée → comptée → refacturée, budgets société / utilisateur et estimation, cloisonnement coût / marge, ticket SSO du mode intégré) ;
- le vrai client HTTP du moteur a été exercé de bout en bout contre un faux fournisseur qui parle le même format d'événements : en-têtes, flux, jetons, prix (7 020 et 3 510 millionièmes attendus et obtenus), réponse coupée, erreurs 401 / 429 / 529, erreur en plein flux, journal des échecs. Aucun appel au vrai fournisseur n'a pu être fait ici (pas d'accès sortant ni de clé) : c'est le premier point de la recette ;
- la bibliothèque
Softic.Intranet.Client.AspNetCorea été branchée dans un mini-produit d'essai : arrivée SSO, cookies de langue et de thème, en-têteframe-ancestors, distinction cadre / onglet direct, rejeu, mauvais destinataire et ticket falsifié refusés, adresse de retour externe ignorée ; - exécution réelle sur SQLite : nouveaux écrans, rôles (SOFTIC / administrateur de société / utilisateur), 4 langues ;
- 1 562 attributs DevExpress contrôlés ; nouveaux usages à surveiller à la première compilation :
DxSpinEditsur undecimal?avecNullText,DxTab.Visible,DxGridDataColumn.Visible; - non vérifiable hors navigateur : le pont de messages entre l'intranet et un produit réel (thème en direct, déconnexion, titre d'onglet), le panneau de l'assistant en interaction, le téléchargement du relevé CSV.
Courrier (lot 4A)
L'intranet n'héberge aucune boîte aux lettres : il se branche sur la messagerie de l'organisation, comme le fait Outlook. Chaque utilisateur relie une ou plusieurs boîtes ; le module Courrier les lit et y écrit en direct, sans copie locale des messages.
Modules/Mail (Web) Engine/Mail Messagerie de l'organisation
┌───────────────────────────┐ ┌──────────────────────────────────────┐ ┌──────────────────────────────────────┐
│ MailPage (3 volets) │ │ MailService boîtes, droits, envoi │ │ Microsoft 365 │
│ MailCompose │──►│ IMailConnector ──► GraphConnector ───┼──►│ Microsoft Graph v1.0 (OAuth2) │
│ MailAccountsDialog │ │ └─► ImapSmtpConnector ┼──►│ IMAP + SMTP : HCL Domino, Exchange │
│ Admin/MailServersAdminPage│──►│ MailAdminService serveurs, essais │ │ sur site, Gmail, Zimbra… (MailKit) │
└───────────────────────────┘ │ MailTokenService jetons Microsoft │ └──────────────────────────────────────┘
/mail/oauth/start|callback │ MailNotificationSink ─► MailOutbox ──┼──► expéditeur système (SMTP ou Graph)
/mail/attachment │ MailOutboxProcessor (reprises) │
└──────────────────────────────────────┘
| Sujet | Décision |
|---|---|
| Fournisseurs | IMailConnector a deux implémentations : GraphConnector (Microsoft 365, HttpClient + JSON, aucun SDK) et ImapSmtpConnector (MailKit, licence MIT). Un troisième connecteur (API REST de Domino, EWS…) n'a rien d'autre à modifier |
| Exchange ActiveSync (1.4.1) | Mail/ActiveSync : la voie d'Outlook et des téléphones, vers HCL Traveler (Domino) ou Exchange — même serveur, mêmes identifiants, rien à ouvrir côté serveur. Wbxml (codage binaire MS-ASWBXML, vérifié sur les trames de référence de la spécification), EasClient (HTTPS + authentification de base ; découverte du point d'entrée /Microsoft-Server-ActiveSync puis /traveler/… ; version commune 14.1 → 12.0 ; redirections suivies à la main, dont 451 ; approvisionnement à la demande sur 449 ou statuts 142-144), EasConnector. Comme Outlook, le connecteur tient une copie de travail par dossier (clé de synchronisation + résumés) mise à jour par différences ; elle vit en mémoire (EasStateCache, partagée entre les écrans d'un utilisateur, jamais écrite en base) et se reconstruit seule si le serveur déclare la clé périmée. Lecture par le MIME complet du message (mêmes images incorporées et pièces jointes qu'en IMAP), envoi par SendMail, lu/drapeau/suppression par Sync, déplacement par MoveItems. Aucune colonne ajoutée : l'hôte occupe ImapHost |
| Agenda Microsoft 365 (1.4.2) | ExternalCalendarService : l'agenda Outlook de l'utilisateur (Graph calendarView, séries déjà développées, heures en UTC) est superposé à son agenda de l'intranet — jamais à celui d'un collègue. Les événements externes sont des CalendarEventDto d'identifiant négatif (ExternalSource = "m365", non modifiables) : les vues existantes les affichent sans changement. Réponse aux invitations (accept / tentativelyAccept / decline), publication d'un événement de l'intranet dans Outlook avec réunion Teams (isOnlineMeeting), marquée de la catégorie « SOFTIC Intranet » pour ne pas revenir en double. Lecture mise en mémoire 45 s ; une panne de Microsoft n'empêche jamais l'agenda de s'afficher |
| Surveillance du courrier (1.4.3) | MailWatchService, exécuté par le service de fond du courrier toutes les 20 s : pour chaque utilisateur connecté (PresenceTracker.OnlineUserIds), la boîte par défaut est relevée au rythme de Relève automatique (30 s au moins) — non-lus pour la pastille du menu (CountersDto.UnreadMail, sujet mail.changed), derniers non-lus pour la cloche (IMailConnector.ListUnreadAsync, notification de catégorie mail, jamais renvoyée par courriel). Premier passage silencieux (apprentissage), un courriel vu n'est jamais notifié deux fois, boîte en panne relevée de moins en moins souvent, boîte d'un utilisateur parti oubliée. Rien en base. Préférence M'avertir des nouveaux courriels (la pastille reste) |
| Réunions natives (1.6.0, lot 4B-1) | Meetings/MeetingRoomService + wwwroot/js/meet.js + Modules/Meet/MeetRoomPage : audio, vidéo et partage d'écran de navigateur à navigateur (WebRTC en maillage, sans serveur de réunion), dans un onglet de l'intranet. L'intranet tient les salles en mémoire (participants par onglet = PeerId), relaie la signalisation (offre, réponse, candidats ICE, « bye ») par son bus temps réel — jamais les flux — et fournit les serveurs ICE : STUN, et TURN avec identifiants temporaires (schéma TURN REST : expiration:softic-<id> + HMAC-SHA1 du secret coturn) ou fixes. Négociation « polie » côté navigateur (le pair au plus petit identifiant cède en cas de collision), candidats mis en attente avant la description distante, partage d'écran par remplacement de piste (pas de renégociation). Capacité Meetings.Native.Max (6) : au-delà la salle est pleine et, si Meetings.BaseUrl (WMeet) est renseigné, la grande salle s'ouvre dans le même onglet (iframe avec allow="camera; microphone; display-capture"). Appels : sonnerie sur toutes les connexions de l'appelé (meet.ring), acceptation → ouverture de la salle, refus → meet.answer, raccroché sans réponse → meet.cancel + notification « appel manqué » (catégorie call). Permissions-Policy : micro, caméra et partage pour self et l'origine WMeet |
| Arabe dans les PDF, circuits fonctionnels (1.16.0, lot 7-3) | Requests/Pdf/TrueTypeFont (cmap 4/12, hmtx, métriques), ArabicText (formes de présentation FE70-FEFF, lam-alef, ordre visuel bidi simplifié), MiniPdf en CIDFontType2 / Identity-H avec ToUnicode, polices Fonts/SofticSans*.ttf incorporées (DejaVu, licence libre). WorkflowTemplate / WorkflowRule, RequestType.TemplateId, RequestStep.PositionId, ApproverKind 6-9, RequestService.ResolveWorkflowAsync et EnsureBuiltInTemplatesAsync |
| Push et PDF natif (1.15.0, lot 7-2) | Push/WebPushProtocol (VAPID ES256, chiffrement aes128gcm : ECDH P-256, HKDF, AES-GCM du framework ; déchiffrement miroir pour les tests), Push/PushService (abonnements PushSubscriptions, envoi en tâche de fond à chaque notification via NotificationService, 404/410 retirés), API /api/v1/push/*, sw.js du mobile (push, notificationclick). Requests/Pdf/MiniPdf (PDF 1.4 : pages A4, Helvetica WinAnsi, retour à la ligne, filets, xref, Info en UTF-16) et RequestPdfService ; archive .pdf à l'approbation ; GET /requests/{id}/pdf (site) et /api/v1/requests/{id}/pdf |
| Application mobile (1.14.0, lot 7) | wwwroot/m/ : PWA autonome (index, app.js écrans et navigation par ancre, api.js jeton + rafraîchissement + cache + file, store.js IndexedDB, i18n.js, sw.js coquille en cache, lib/jsQR.js). Idempotence côté serveur : TimeEntry.ClientId, Request.ClientRef, heure réelle bornée (ClockRequest.AtUtc). Route /m servie hors du repli Blazor. Détail : docs/MOBILE.md |
| Sondages et pointage (1.13.0, lot 6-3) | Engagement/SurveyService (cibles par unité et sous-unités, participation séparée de la réponse pour l'anonymat, résultats agrégés), Engagement/AttendanceService (sites avec rayon et secret QR, jeton du jour HMAC-SHA256, distance haversine, règles arrivée/départ, journées et anomalies calculées à la lecture avec fériés et congés, équipe via N+1, saisie manuelle auditée, export). Modules Sondages et Pointage, softicGeolocate dans intranet.js, API /api/v1/surveys/* et /api/v1/attendance/* |
| Notes de frais, impression, congés (1.12.0, lot 6-2) | Requests/LeaveService (jours ouvrés, soldes LeaveBalances), RequestPrintService (document HTML autonome, échappement lisible), champ de formulaire lines (ExpenseLines, total = montant), type livré EXPENSE avec étape finance.approve, RequestService.OnApprovedAsync (archive StoredFile.RequestId + événement d'absence via CalendarService au nom de la personne, UserContextFactory), ExportExpensesCsvAsync. Onglet Congés du dossier ; softicPrint / softicDownloadText dans intranet.js |
| Demandes et circuits (1.11.0, lot 6-1) | Requests/RequestService : types par société (FieldsJson, StepsJson, seuils de montant), brouillon validé contre le formulaire, dépôt avec circuit instancié (N+1 / N+2 par OrganizationService.ManagerChainAsync, RH et droits par RolePermissions, personne nommée, délégations ApprovalDelegation, étapes sautées si sans approbateur ou si l'approbateur est le demandeur), décision, refus motivé, annulation, commentaire, réattribution, rappels quotidiens (Requests.ReminderDays), notifications à chaque transition, temps réel request.changed, pièces jointes StoredFile.RequestId. Droit requests.manage. Module Demandes avec formulaire généré depuis le type et éditeur de types ; pastille « à valider » (CountersDto.RequestsToApprove) ; API /api/v1/requests/*. Détail : docs/DEMANDES.md |
| Compétences et performance (1.10.0, lot 5-3) | HumanResources/CompetenceService : référentiels par société (compétences, certifications avec validité et caractère obligatoire), niveaux évalués par le N+1 ou la RH, certifications détenues avec expiration calculée, états valide / bientôt expirée / expirée, alertes quotidiennes à seuils (Hr.AlertDays) vers la personne, le N+1 et la RH, objectifs pondérés, entretiens (auto-évaluation → évaluation → clôture). EmployeeService.SyncFromUsersAsync : dossiers créés d'office pour les utilisateurs internes (Hr.AutoEmployees, Hr.ExternalRoles, AppUser.IsExternal, Hr.NumberFormat), à la mise à niveau, à chaque enregistrement d'utilisateur et une fois par jour (MailOutboxWorker). Onglets Compétences et Objectifs et entretiens du dossier, Référentiels pour la RH, API /api/v1/hr/employees/{id}/skills|certifications|objectives|reviews, /api/v1/hr/certifications/expiring |
| Dossier employé (1.9.0, lot 5-2) | HumanResources/EmployeeService : dossier par personne et société, contrats, documents (StoredFile.EmployeeId, lecture réservée à l'employé et à la RH), photo, sortie qui clôt affectations et contrat, effectifs. Niveaux d'accès Full / Self / Reduced / None calculés avec le N+1 de l'organisation ; audit hr.view et hr.reveal ; champs sensibles chiffrés par ISecretProtector. Module Ressources humaines, API /api/v1/hr/*. Détail : docs/RH.md |
| Organisation (1.8.0, lot 5-1) | Organization/OrganizationService : unités typées (OrgUnitKind), postes (JobPosition, ligne hiérarchique des postes), affectations datées (Assignment : principale, N+1 explicite, clôture automatique de la précédente, recopie du rattachement et de l'intitulé pour l'annuaire), organigramme (ChartAsync), résolution du N+1 et de la chaîne hiérarchique (ManagerChainAsync), équipe d'un responsable ; droit hr.manage ; sujet temps réel org.changed ; API /api/v1/organization/* en lecture pour les produits SOFTIC. Module Organisation : organigramme en boîtes, fiche d'unité, postes, parcours. Plan complet du lot 5 : docs/RH.md |
| Téléphone SIP (1.7.0, lot 4B-2) | Meetings/TelephonyService + wwwroot/js/sip.js + wwwroot/lib/jssip (JsSIP 3.13.8, MIT, bundle esbuild) + Shell/Softphone. Poste, identifiant et mot de passe SIP dans les préférences utilisateur (mot de passe chiffré par ISecretProtector), profil servi au navigateur avec les serveurs ICE des réunions ; enregistrement, appel, réponse, sourdine, attente, DTMF dans le navigateur ; un appel à la fois (second entrant → 486 occupé). Journal des appels : table CallLogs (une ligne par participant), alimentée par le combiné et par les appels internes du lot 4B-1 (refusé, manqué, annulé, répondu + durée). Relais TURN loué : Meetings.TurnProvider = cloudflare (/v1/turn/keys/{id}/credentials/generate-ice-servers) ou metered (/api/v1/turn/credentials), identifiants obtenus par le serveur et gardés une heure |
| Planner, OneDrive, SharePoint (1.7.0, lot 4D-2) | Mail/GraphClient (appel Graph au nom de l'utilisateur, permission demandée à part, ETag/If-Match, erreurs traduites), PlannerService (/me/planner/tasks, plans et compartiments en cache 5 min, PATCH avec If-Match, création assignée via m365.id), CloudFilesService (/me/drive, sharedWithMe avec remoteItem, recent, search, /sites, /drives). Modules : volet Planner dans Tâches, page Fichiers 365 |
| Connexion Microsoft (1.5.0) | MicrosoftSignInService : /account/microsoft/start → autorisation Microsoft (locataire de la fiche, mêmes permissions que la boîte) → /account/microsoft/callback → échange du code, /me (identifiant d'objet, adresse, UPN) → utilisateur de l'intranet retrouvé par la préférence m365.id, sinon par l'adresse → cookie de session habituel (AccountSignIn), boîte reliée du même coup, audit auth.login. État chiffré + aléatoire en cookie (10 min) ; aucun compte créé ; mot de passe de l'intranet toujours accepté |
| Présence Teams (1.5.0) | TeamsPresenceService : getPresencesByUserId avec le jeton du visiteur, permission Presence.Read.All demandée à part de celles du courrier (un refus ne prive jamais le courrier), correspondance par m365.id, cache d'une minute, silence de dix minutes après une erreur de Microsoft |
| Protocoles facultatifs | réception (IMAP) et envoi (SMTP) sont indépendants : un hôte vide = protocole ignoré. Serveur sans IMAP → boîtes en envoi seul (aucune tentative IMAP, vérification du compte par authentification SMTP) ; serveur sans SMTP → lecture seule, et il ne peut pas être l'expéditeur des notifications |
| HCL / Lotus Domino | par IMAP + SMTP : tâches imap et smtp actives sur le serveur, TLS, mot de passe Internet de la fiche Personne. Dossiers repérés par leur nom quand le serveur n'annonce pas SPECIAL-USE |
| Connexion IMAP | gardée ouverte le temps de l'écran (une ouverture coûte près d'une seconde), une commande à la fois, reprise automatique si le serveur l'a coupée |
| Microsoft 365 | consentement OAuth2 de chaque utilisateur (/mail/oauth/start → Microsoft → /mail/oauth/callback). L'état, chiffré, lie le retour à l'utilisateur parti. Seul le jeton de rafraîchissement est stocké (chiffré) ; le jeton d'accès reste en mémoire. Réponses et transferts passent par les brouillons de Graph : Exchange construit la citation et le fil de discussion |
| Secrets | mots de passe IMAP, jetons, secret client, mot de passe de l'expéditeur : ISecretProtector (Data Protection), jamais renvoyés à l'interface |
| Affichage du HTML | règle de sécurité : le HTML d'un message n'entre jamais dans le document de l'intranet. Il est rendu dans un cadre isolé (iframe sandbox sans scripts ni origine, document autonome passé par srcdoc, politique default-src 'none'). Images distantes bloquées jusqu'à la demande de l'utilisateur ; images incorporées (cid:) converties en data:. En réponse ou transfert, la citation est ajoutée par le serveur : l'éditeur ne contient que le texte de l'utilisateur |
| Pièces jointes | servies par /mail/attachment, en téléchargement, sous Content-Security-Policy: sandbox ; une boîte n'est ouverte que par son propriétaire |
| Courriels de notification | INotificationSink branche des canaux sur le centre de notifications. MailNotificationSink écrit en file (MailOutbox) selon les réglages et les préférences de chacun ; MailOutboxProcessor (service de fond, 20 s) envoie avec reprises à 1, 5, 15, 60, 240 et 720 minutes. Un canal défaillant ne bloque jamais la notification |
| Agenda | les notifications d'invitation portent une pièce invitation.ics (RFC 5545, METHOD:PUBLISH, même UID et SEQUENCE croissant pour une mise à jour) : l'événement s'inscrit dans Outlook, Notes, Gmail |
Lot 8 — Application native et API sortante
src\Softic.Intranet.Mobile: application .NET MAUI (Android, iOS), hors de la solution principale, cliente deapi/v1comme la PWA ; téléphone SIP par SIPSorcery sur WebSocket avec audio natif (Services\Telephony,Platforms\Android,Platforms\iOS) ; MVVM CommunityToolkit ; SQLite local (cache, file d'actions) ; voirMOBILE-NATIF.md.Engine\Integration\IntegrationReadService: lectures pour les produits (unités avec chemin et responsable, postes, effectifs avec affectation principale du jour et responsable, certifications avec état) ; exposé parIntegrationControllersous clé d'intégration avec les portéesorganizationetcertifications; contrats dansShared\Contracts\IntegrationContracts.cs; bibliothèqueSoftic.Intranet.Client1.4.0 (GetOrganizationAsync,GetPeopleAsync,GetCertificationsAsync,HasValidCertificationAsync).Api\Controllers\MeetingsController:telephony/profileettelephony/callspour l'application native.
Lot 10 : facturation électronique (v1.19.0)
- Domaine :
InvoicingProfile(paramètres par société, secrets chiffrés),InvoiceTaxRate,InvoiceCustomer,InvoiceItem,Invoice(brouillon → émise → signée → déposée → acceptée / rejetée / annulée),InvoiceLine,InvoiceDocument(XML légaux + SHA-256),InvoiceEvent(historique). Droitsinvoicing.use,invoicing.issue,invoicing.manage. - Moteur (
Engine/Invoicing) : modèle pivotEInvoice(EN 16931),InvoiceCalculator(fonction unique de calcul),AmountInWords,InvoiceHtml(impression, QR TTN), packs paysIInvoiceCountryPack(TunisiaPack: contrôles, TEIF 1.8.8, noms de fichiers, lecture de l'accusé),IInvoiceSigner(XmlSignerClient, XAdES),IInvoicePlatform(ElFatouraClient, SOAP),InvoicingService(cycle de vie, numérotation, chronologie, documents, diagnostic) etInvoicingWorker(suivi des dépôts à délais croissants). - API :
api/v1/invoicing(création idempotente parexternalRef, émission, dépôt, suivi, documents, impression). - Web : module
/invoicing(factures, clients, articles, paramètres) ; documents servis par/invoicing/{id}/document/{docId}(un document précis, l'historique restant consultable après une remise en brouillon) ou/invoicing/{id}/document/{kind}(le dernier de cette nature). Ces points d'entrée répondent 403 ou 409 (empreinte altérée, journalisée), jamaisResults.Forbid(), qui renverrait en boucle un utilisateur connecté vers la page de connexion. - Temps réel : sujet
invoice.changed; notifications d'acceptation et de rejet à l'émetteur. Détails :docs/FACTURATION.md.
Lot 11 : menu général par processus (v1.20.0)
- Principe : le menu présente d'abord les services communs (visibles de tous), puis un groupe repliable par processus métier (Administration, Qualité, Ressources humaines, Finance, Études…) visible des seuls rôles qui lui sont rattachés. Un processus regroupe des modules de l'intranet et des applications du lanceur, chacun partagé (reste aussi un service commun) ou réservé.
- Domaine :
BusinessProcess(code, nom en 4 langues, icône, ordre, actif),BusinessProcessRole,BusinessProcessEntry(clé de moduleModuleCatalogou application,Exclusive). Processus communs à toutes les sociétés, comme les rôles. - Moteur :
ProcessServiceporte la règle unique (MenuForAsync,ReservedAppsAsync), appliquée aussi parAppService(lanceur),SearchServiceetSsoService(/launch/{id}).UserContext.RoleId: rôle de la société de travail. - Web :
UserSession.Menu/IsModuleVisible(menu, palette Ctrl+K,WorkspaceService.Visibilitypour les liens directs),NavPaneréécrit (groupes repliables mémorisés dansui.navClosed, pastilles cumulées), écran Administration → Processus (droitadmin.roles). Sujet temps réelprocesses.changed: chaque fenêtre recalcule son menu sans recharger. - Mise à niveau : processus livrés créés une fois (
System.Upgrade.Lot11Processes), rôles rattachés d'après leurs droits existants. Détails :docs/MENU-PROCESSUS.md.
Lot 12 : pont ERP d'EPPM, côté intranet (v1.21.0)
Principe. L'ERP d'EPPM (IES) reste maître des employés et de l'organisation jusqu'à la bascule. L'agent
Softic.Intranet.Agent, en lecture seule chez EPPM, pousse l'organisation, les employés et la présence.- L'intranet les reçoit sous
api/integration/v1/hr/import/*, avec une cléhr.importexclusive et un seul client autorisé. - Il les rattache à une seule société, celle de
Integrations:Erp:CompanyCode, marquée « gérée par IES ». - Contrat :
docs/INTEGRATION-ERP.md.
- L'intranet les reçoit sous
Domaine.
Company.HrManagedBy: marquage de la société, posé par le seul super-administrateur.ExternalCodeetManagedBysurOrgUnit,JobPositionetAssignment, avec des index uniques filtrés.- Sur
Employee:SiteCodeetSiteCodeSource,ManagedBy,ExitDateEstimated(P2),ErpMissingSince(présence confirmée sur deux listes). IntegrationBatch: journal des lots, avec empreinte, approbation et références d'audit.
Moteur.
HrImportServiceporte tout le contrat :- lecture du corps élément par élément, et rejets ligne par ligne avec leur rang et leur motif ;
- upsert sans écriture quand rien ne change, et un seul
SaveChangespar ligne (ou par instantané), sans transaction explicite, ce qui est compatible avecEnableRetryOnFailure; - verrou d'un lot à la fois, pris après les contrôles en lecture seule (corps, configuration, marquage) ;
- journal tenu ligne à ligne : un lot interrompu est journalisé « cancelled » avec ce qu'il a écrit ;
- garde-fous (racine d'organigramme, sorties du jour, changements de site) avec approbation « à l'identique » par le super-administrateur ;
- sortie comptée une seule fois ; accès coupé le jour où elle prend effet, sans toucher
IsActive, que Badge lit, et sessions ouvertes fermées (SessionRevoked).
La règle de verrouillage des fiches est
ErpManaged.IsLocked, appliquée parEmployeeService,UserService,OrganizationServiceetCompanyService. Les services gardent la valeur importée au lieu de refuser l'enregistrement, et refusent les suppressions. Un dossier géré sorti ferme la société au compte (UserContextFactory.ExitedCompanyIdsAsync, même après la bascule) et le retire de l'annuaire (DirectoryService) ; il ne verrouille plus le compte.Web.
- Écran Administration → Intégrations : état du pont, journal, approbation.
- Case « Référentiel RH géré par IES » dans Sociétés, et portée Import du référentiel RH dans Clients d'intégration.
- Bandeau « Géré par IES » et champs importés en lecture seule dans la RH, l'organigramme, les utilisateurs et le profil.
- Politique de limitation de débit
integration.
Configuration. Section
Integrations:Erpd'appsettings.Production.json, relue à chaud (IOptionsMonitor) :CompanyCode,ClientAppCode,AllowedIps,RoleName,ImportEmails,SiteCodes,SiteMap(ports, plages, racines).
Pont ERP, étape 2 : agent Softic.Intranet.Agent (agent 1.0.0, intranet v1.21.1)
Cette section décrit l'agent 1.0.0. Il est remplacé par l'agent 1.1.0 (lot 13, étape 5 : section « Lot 13 », plus bas), seul compatible avec l'intranet 1.22.0.
- Place. Service Windows installé chez EPPM, sur VMS-INTRANETAPP, à côté de l'agent Softic.Badge, dont il reprend la
conception : hôte .NET 8 minimal, instance unique,
state.json, installeur PowerShell, paquet autonome avec empreinte.- Il ne référence aucun projet de la solution : il ne connaît que le contrat JSON (
docs/INTEGRATION-ERP.md), dont il redéclare les DTO (Relay/ImportContracts.cs). - Notice d'installation et d'exploitation :
docs/AGENT-ERP.md.
- Il ne référence aucun projet de la solution : il ne connaît que le contrat JSON (
- Couches.
Erp/ErpSource.cs: seules requêtes SQL de l'agent. Ce sont desSELECTconstants et paramétrés, préfixés deREAD COMMITTED,LOCK_TIMEOUT 5000etDEADLOCK_PRIORITY LOW, sur les colonnes de la liste blanche du login (docs/SQL-LOGIN-LECTURE-ERP.sql). On y trouve aussi le contrôle des droits du login (PermissionsSql), refait à chaque cycle.Erp/ErpRules.cs: règles du relevé (docs/ERP-SCHEMA-RH.md§ 9) → instantané au format du contrat :- clés externes P7, affectation principale P8, périmètre P3 monotone, sentinelles ;
- site dominant d'après les ports de pointage et la table
sitePortspubliée par le serveur ; - détection d'une lecture instable.
Relay/*: client HTTP (cléX-Api-Key, aucune redirection suivie, certificat toujours vérifié) et sérialisation. L'empreinte d'un lot est le SHA-256 des octets envoyés, celle que calcule le serveur.State/AgentStateStore.cs: état versionné, enregistré atomiquement avec une copie.bak. Il contient les empreintes par ligne, les lots retenus, la référence du garde-fou de variation, les curseurs de pointage et les sites. Un état d'une version plus récente que l'agent est refusé.Sync/ErpSync.cs: cycle (état du pont, droits, lecture, organisation, employés, signal de vie, présence) et garde-fous de l'agent :- variation de plus de 10 % des clés, ou de plus de 50 affectations ;
- clôture d'affectation confirmée par deux lectures ;
- organisation relue 75 secondes après un changement.
Erp/ErpCheck.cs: commandeerp-check. Elle lit, compte et compare aux PIN de Softic.Badge, sans rien envoyer.
- Serveur (v1.21.1).
GET hr/import/statuspublie la table des ports deIntegrations:Erp:SiteMap(sitePorts) quand elle est valide. L'agent calcule ainsi le site dominant par site et non par port, sans dupliquer la correspondance chez EPPM. Le journal des lots (IntegrationBatchDto.PayloadHash) et l'écran Intégrations montrent l'empreinte que cite l'agent. - Décisions du 5 octobre (v1.21.2).
- P31.
HrImportService.PredictClosuresAsyncrejoueSyncAssignmentssur des copies non suivies, avant toute écriture, pour compter les clôtures d'affectation d'employés actifs. Au-delà deMaxClosuresPerDay(50) par jour, cumulées parIntegrationBatch.Deactivateddes lots d'employés, le lot est retenu. - P22.
DirectoryService.Present, partagé avecSearchService, écarte les comptes gérés jamais ouverts. Le paramètreincludeUnopeneddes listes de choix les garde pour les écrans de gestion.PersonDto.Loginest vide pour un compte géré dont l'identifiant est le matricule. - P23. Permission
hr.accounts.ManagedAccounts.RefusalAsyncporte la règle unique d'ouverture : dossier, compte, rattachements, compte déjà ouvert, et rôle contenu dans les droits de l'appelant. Elle est partagée par le drapeauEmployeeDto.AccountOpenable(bouton de la fiche RH) et parUserService.OpenManagedAccountAsync, qui ouvre le compte par une écriture conditionnelle, avec un mot de passePasswordPolicy.GenerateetMustChangePassword.
- P31.
- Suite de la v1.21.2 (v1.21.3).
- P34, changement du mot de passe imposé partout.
AuthService.CheckSessionAsyncrenvoieSessionState(Invalid,Valid,PasswordChangeRequired) en une requête. Elle est appelée à chaque requête web (OnValidatePrincipal) et à chaque appel de l'API (SofticBearerHandler).UserContext.MustChangePasswordporte l'indicateur.- Web :
OnValidatePrincipalpose la marquePasswordChangeGate.Item. Un intergiciel placé aprèsUseAuthorizationrenvoie alors tout chemin hors dePasswordChangeGate.IsAllowedvers/Account/ChangePassword(401 pour/_blazor). La page n'a plus « Annuler », seulement la déconnexion (formulaireasp-page, jeton anti-falsification). - Fenêtres déjà ouvertes : la case cochée par
UserService.SaveAsyncpublieSessionRevoked(rechargement immédiat). Le changement du mot de passe ne publie rien, pour ne pas faire recharger un onglet voisin avec l'ancien cookie. À défaut de signal,RevalidatingAuthStateProvideréchoue en 2 minutes au plus ; une erreur de la base n'est pas un échec.MainLayout, qui n'affiche pas leBodyd'AuthorizeRouteView, recharge alors la page (AuthenticationStateChanged).intranet.jsrecharge aussi une fenêtre dont la reconnexion est refusée alors que/healthrépond. - API : aucun jeton.
login,exchangeetrefresh(TokenService.RenewAsync) répondent 403password_change_required.IssueAsyncle refuse aussi, par garde-fou. Un jeton déjà délivré est refusé, et le hub aussi. - Microsoft 365 : la revendication
amr=microsoftdu cookie (AccountSignIn.MethodMicrosoft) permet àUserService.ChangeOwnPasswordAsync(..., provenByMicrosoft)de remplacer un mot de passe imposé sans l'ancien, pour un compte lié à Microsoft. Le nouveau mot de passe doit différer de l'actuel (Password.SameAsCurrent). - Clients : PWA 1.21.3 et application native 1.21.3 (code 6). Ils affichent le motif et un lien vers la page web,
gardent la file hors ligne et y mettent l'action refusée ; le rejeu s'arrête quand la session tombe. PWA : aucun
écran ne recouvre la connexion sans jeton ; un refus ordinaire du rafraîchissement reste sans statut (mise en
file, comme avant) ;
store.jsrendundefinedpour une clé absente (défaut antérieur : la requête IndexedDB passait pour des jetons et la PWA restait sur « Chargement »).
- P38.
ManagedAccounts.RefusalAsync(..., regenerate: true)accepte un compte ouvert par un mot de passe initial jamais utilisé : aucune connexion, changement encore dû, aucune liaison Microsoft.UserService.RegenerateInitialPasswordAsyncle remplace par la même écriture conditionnelle que l'ouverture. Le drapeauEmployeeDto.AccountPasswordRegenerableaffiche le bouton de la fiche RH. - P37.
TaskEditorgarde le responsable actuel d'une tâche hors espace quand la liste ne le propose plus.
- P34, changement du mot de passe imposé partout.
- Tests.
Softic.Intranet.Agent.Tests(xUnit), avec des doubles de l'ERP, de l'intranet et de l'horloge :ReadOnlyGuardTests:SELECTseulement, colonnes et fonctions en liste blanche, aucune commande SQL horsErpSource, quatre routes, aucun contournement de certificat, aucun secret dans les messages ;- règles et contrat ;
- scénarios de cycle (lots retenus, garde-fous, présence, reprise) ;
- options, verrou et état.
Lot 13 : socle RH d'entreprise (v1.22.0, livrable après la recette ProdEssai)
Mission : docs/MISSION-LOT13-SOCLE-RH.md ; décisions et étapes : docs/COMPTE-RENDU-LOT13.md.
- Trois niveaux.
Person(tablePeople, identité, une fois par groupe),Employee(relation d'emploi, une par personne et par société),AppUser(compte facultatif). Le groupe d'une personne est sa société racine (Person.TenantCompanyId, société sansParentCompanyId). - Étape 2 : modèle et migration additive
Lot13_SocleRh.Person: identité du § 1.1 de la mission,UserId(compte de la personne dans ce groupe, unique par groupe : un même compte peut avoir une personne dans deux groupes),ExternalCode(IES:M<matricule>) etNationalIduniques par groupe, index de rapprochement (groupe, nom, prénom, naissance).Employee.PersonId,ExternalCode(unique par société),JobTitle;Assignment.EmployeeId,ManagerEmployeeId;OrgUnit.ManagerEmployeeId;Company.ExternalCode;EmploymentContract.CompanyIdetExternalCode(unique par société) ;ApiClient.TenantCompanyId(clé bornée à un groupe).- Comptes repris :
AppUser.LegacySource,LegacyUserId(uniques ensemble),LegacyImportedUtc(premier import),LegacyActive(état dans IES, distinct d'IsActive),LegacyPasswordChangedUtc(reprise de l'empreinte seulement si le mot de passe a changé dans IES),LegacyLastLoginUtc(comptes dormants). Ouverture par société :UserCompany.PendingSinceUtc(appartenance créée par l'import et pas encore ouverte ; nul = ouverte). Employee.UserIdetAssignment.UserIddeviennent facultatifs (employé sans compte) ; leurs index uniques ne portent plus que sur les lignes qui en ont un. Les champs d'identité sont restés aussi surEmployeejusqu'àLot13b_SocleRh_Nettoyage(lot 14, étape 2, ci-dessous), qui les a supprimés avec ces deux colonnes.- Toutes les nouvelles relations sont en
Restrict: supprimer une société, un compte ou un fichier ne supprime jamais une personne. Jusqu'àLot13b(1.22.0), supprimer un compte supprimait encore, par la cascade du lot 5-2 surUserId, ses dossiersEmployees(avec contrats, certifications et documents) et sesAssignments: avant de supprimer un compte, il fallait poserUserId = NULLsur ses employés, ses affectations et sa personne (People.UserId). DepuisLot13b, seulPeople.UserId(relationRestrict) relie un compte à ses dossiers. - Sociétés : un seul groupe EPPM ; les sociétés hors arbre d'IES (sans parent, autres que la racine) qui ont un
actif ou une organisation en vigueur sont rattachées à « Groupe EPPM » (
attached_to_root), les autres sont ignorées (ALGALIA) : décision du 6 octobre. - Les services n'utilisent pas encore ces colonnes, mais tolèrent un employé ou une affectation sans compte : listes
et dictionnaires filtrés sur
UserId, sortie limitée aux affectations de l'employé, liste RH au nom de la personne. Un dossier sans compte n'est visible que de la RH (EmployeeService.AccessAsync) ; les DTO gardent leur forme (UserId = 0pour un employé sans compte). Règle : jamaisa.UserId == b.UserIdentre deux colonnes facultatives (EF le traduit en « tous les NULL »).
- Étape 3 : amorçage, services et écrans (détail et relectures :
docs/COMPTE-RENDU-LOT13.md, § 3.1 à 3.6).- Utilitaires.
Companies/CompanyTree: racine d'une société (son groupe), groupe d'une société (GroupOfAsync), détection de cycle.HumanResources/PersonLinks: personne d'un compte dans un groupe.Organization/OrgHierarchy: instantané hiérarchique d'une société (unités, employés avec personne et compte, affectations principales du jour, appartenances, comptes cités hors appartenance), chargé en cinq requêtes au plus, puis N+1, chaînes, équipes et responsables d'unité calculés en mémoire. - Droits.
hr.people(identité des personnes, vue Personnes) ethr.contracts(contrats, vue Contrats), donnés par la mise à niveauSystem.Upgrade.Lot13Permissionsaux rôles qui onthr.manage. Sans eux, la RH garde le dossier d'emploi (EmployeeDto.IdentityVisible,ContractsVisible). - Amorçage
System.Upgrade.Lot13(Seed/Lot13Upgrade, supprimé à l'étape 2 du lot 14 une fois joué partout où la 1.22.0 est passée, la clé restant posée parDbSeeder; une fois, dans une transaction, avec la stratégie de reprise) : une personne par compte et par groupe, une par dossier sans compte ; affectations reliées à leur employé (orphelines internes : employé créé ; orphelines externes : unité et intitulé reportés sur l'appartenance, puis supprimées) ; responsables en employés ; intitulés et société des contrats ; comptes « !ERP! » du lot 12 jamais utilisés et non référencés supprimés ; contrôles finaux (aucun employé sans personne, aucune affectation sans employé, groupe de la personne = racine, compte du dossier = compte de la personne), sinon arrêt. - Services par personne et employé.
EmployeeService: identité sur la personne (maître des noms, recopiés sur le compte), trois modes de création (compte, personne du groupe, personne nouvelle), fiche personne, vue Contrats, accès par employé (AccessToAsync).UserService: « Ouvrir le compte » d'un dossier sans compte (identifiant = matricule), liaison et déliaison compte ↔ personne, ouverture d'une appartenance en attente, externes (rôle externe et unité).UserCompany.PendingSinceUtcnon nul : la société ne s'ouvre pas au compte et il n'apparaît nulle part hors des écrans de gestion. - N+1 par employé (remplace la règle « dossier sans compte visible de la RH seule » de l'étape 2 : son N+1 le
voit aussi). Façades par compte gardées (
ManagerChainAsync,TeamAsync,HistoryAsync) ;ManagerChainDto.ManagerUserId= compte utilisable du N+1 (actif, appartenance ouverte, employé non sorti),Levels= tous les niveaux. Approbations : un responsable prévu présent sans compte utilisable (sauf délégation en cours vers un compte utilisable, suivie d'abord) fait aller l'étape à la RH (hr.manage), à défaut de RH autre que le demandeur au gestionnaire des demandes (requests.manage), son nom en tête du commentaire, sans seconde décision du même droit ; elle n'est sautée que si personne ne porte ni l'un ni l'autre droit, ou sans responsable (sommet). L'API d'intégration et les alertes de certification prennent le même N+1. - Groupe. Annuaire (avec les employés sans compte), recherche, listes de choix et API
directorylimités au groupe de la société courante ; messagerie, agenda, invitations, espaces et appels n'atteignent que les comptes joignables du groupe (DirectoryService.ReachableAsync) ; le super-administrateur voit tout. Arbre des sociétés : pas de cycle, pas de changement de groupe pour une société qui a des employés ou des personnes. - Écrans. Organisation (responsables et personnes affectées par employé, avec ou sans compte), RH (vues Dossiers, Personnes, Contrats ; fiche personne ; création en trois modes), annuaire (personnes sans compte), administration des comptes (externe, appartenance en attente, personne liée), demandes (étape repliée).
- Utilitaires.
- Étape 4 : import IES révisé (aucune migration ; conception
docs/LOT13-ETAPE4-CONCEPTION.md, contratdocs/INTEGRATION-ERP.md, réalisation : compte rendu, § 4.0 à 4.9).HrImportService, classe partielle (Engine/HumanResources) : fichier principal (adresses, client, journal et approbation, plafonds du groupe, cadreRunBatchAsync, outils de lecture) et une partie par sujet :Scope(groupe de la clé, société de l'appel, configuration commune, société par défaut),Companies,Organization,People,Employees(avec la coupure d'accès),Presence,Contracts,Accounts,Opening(ouverture groupée des comptes importés) etStatus(état du groupe et de chaque société). Contrat partagé :Shared/Contracts/HrImportContracts.cs; contrôleur :Api/Controllers/HrImportController.cs.- Routes sous
api/integration/v1/hr/import/: routes de groupecompanies,people,accounts,employees/presence(journalisées sous la racine) ; routes par sociétéorganization,employees(contrat v2, sans identité ni compte) etcontracts(société du contrat), avec?companyExternalCode=IES:S<Id>obligatoire ;GET status. La cléhr.importest bornée à une racine (ApiClient.TenantCompanyId, posée par le super-administrateur,ApiClientService.BindAsync) : c'est le groupe de tous ses appels. L'ouverture des comptes importés n'a pas de route : elle se fait dans Administration > Intégrations (OpenImportedAccountsAsync) ou une par une (UserService.OpenMembershipAsync). - Règle « lecture par la personne » (généralisée à tout le code par
Lot13b) : le code de l'étape 4 lit le compte d'un dossier par sa personne (People.UserIddeEmployee.PersonId) et les dossiers d'un compte par ses personnes ; jusqu'àLot13b,Employee.UserIdetAssignment.UserIdn'y étaient plus qu'écrits, en copie, par un point unique,Engine/Users/PersonAccounts.MirrorAsync(appelé parUserServiceet par les routesemployeesetaccounts), parce queUserContext, l'annuaire et le verrou E9 les lisaient encore.Lot13ba supprimé ces colonnes sans casser l'import ;MirrorAsyncne tient plus que le miroirOrgUnit.ManagerUser. - Coupure d'accès commune (
CutAccessAsync,HrImportService.Employees.cs), appelée à toute sortie effective paremployeesetpresence, dans leSaveChangesde la sortie, décidée par l'état : le compte n'est coupé que si la personne n'a plus aucun dossier non sorti dans le groupe (une mutation garde l'accès), qu'il ne lui reste aucune société ouvrable selon la règle deUserContextet qu'il reste quelque chose à couper ;IsActiven'est jamais touché. Elle remplace la règle du lot 12, qui ne coupait jamais un compte rattaché à une autre société. - Autres règles transverses : plafonds des garde-fous comptés sur le groupe, à l'heure de la racine, par type de
lot ; un
SaveChangespar ligne (un seul pourcompaniesetorganization), lectures préchargées par lot ; aucune ligne inchangée n'écrit ;Companies/CompanyTree.ProjectedRoots(arbre projeté de la routecompanies) ;PasswordHasher.IsWellFormed(empreinte reçue d'IES) ; comptes importés (AppUser.Legacy*) créés inactifs, avec des appartenances en attente (UserCompany.PendingSinceUtc) ; l'amorçageLot13Upgrade(1.22.0) ne supprimait jamais un compte repris. - Écrans : Intégrations (état du groupe, grille par société, ouverture groupée avec aperçu, journal avec la
colonne des lignes ignorées) ; Clients d'intégration (colonne Groupe) ; Utilisateurs (dernière connexion à IES,
refus de déliaison d'un compte partagé entre groupes,
Error.UnlinkSharedAccount). - Hors étape 4 : agent 1.1.0 (étape 5, ci-dessous ; l'agent 1.0.0 n'envoie pas ce contrat) ; lecture de
Softic.Badge par groupe (reportée à la liaison Badge du lot 15) et
Lot13b(livrée avec le lot 14, étape 2 : décision du 10 octobre, deux livraisons).
- Étape 5 : agent ERP
Softic.Intranet.Agent1.1.0 (conceptiondocs/LOT13-ETAPE5-CONCEPTION.md, noticedocs/AGENT-ERP.md, contratdocs/INTEGRATION-ERP.md§ 17). Projet toujours autonome (aucune référence à la solution ; copie du contrat v2 dansRelay/ImportContracts.cs, concordance vérifiée par réflexion avec Shared).- Sources, lecture seule :
Erp/SqlReadOnly.cs(seul point d'exécution SQL, session imposée, un seulCommandText),Erp/ErpSource.cs(baseserpet Pointeuse) etErp/DnnSource.cs(baseINTRANETde DNN, facultative) ; liste blanche, droits et colonnes sentinelles contrôlés par base ;ReadOnlyGuardTestspar base. - Règles :
Erp/ErpRules.cs(règles du lot 12),Erp/GroupRules.cs(arbre et sociétés envoyables, société d'emploi de Q2, dossiers monotones, mutations, contrats ; fonctions pures),Erp/AccountRules.csetErp/DnnAccountScan.cs(comptes DNN : rapprochement par matricule, externes, techniques prédits, courriels). - Cryptographie :
Crypto/(lecture de lamachineKeydans leweb.configd'IES, dont seul le chemin est en configuration, décision E5-Q2 ; déchiffrement 3DES et AES duSqlMembershipProvideren tampons épinglés ; empreinte PBKDF2-SHA512 au format de l'intranet). Sans journal ni écriture de fichier ; servi parerp-checkseulement : la reprise des mots de passe se fait hors cycle, paraccounts-checketaccounts-migratesur le moduleSecurity/(fourni par Claude Fable, étape 6.1), qui écrit un fichier d'empreintes consommé par la routeaccounts(Sync/HashFileAccountPasswordSource.cs,Agent:AccountsHashFile). - Cycle :
Sync/ErpSync*.cs, classe partielle, une partie par route :companies,organizationpar société,people,employeespar société en deux passes (sortie d'origine d'une mutation après l'arrivée),contracts,accounts(envoi réel avec empreintes quandAgent:AccountsHashFileest renseigné, simulation seulement sinon ;IAccountPasswordSourcefourni parHashFileAccountPasswordSource), présence de groupe et signal de viepeople []; classement des réponses (cycle, société sautée,accountsseule, lot retenu) ; paliersonce --routes … [--company …](code 5) etdry-run --routes …. - Débit :
Relay/IntranetClient.cs, huit routes, porte unique (un appel à la fois, au moins une seconde entre deux départs, horloge monotone) ; 83 appels mesurés pour un premier envoi complet sansaccounts. - État et configuration :
state.jsonau format 2 (sociétés, organisations, personnes, dossiers par société, contrats, comptes, lots retenus ; rien de dérivé d'un mot de passe), état 1.0.0 mis de côté enstate.json.v1; configuration au format 2 (DnnConnectionString,IesWebConfigPath,DnnPortalId,TechnicalLogins,MinRequestIntervalMs), clé de déchiffrement refusée en configuration ; installeur-IesWebConfig,-GrantIesWebConfigRead.
- Sources, lecture seule :
Lot 14 : compléments du socle RH (v1.23.0)
Mission : docs/MISSION-LOT14-COMPLEMENTS-SOCLE-RH.md ; conception : docs/LOT14-CONCEPTION.md ; décisions et étapes :
docs/COMPTE-RENDU-LOT14.md. Lot terminé côté Claude le 11 octobre 2026 (étapes 1 à 7) ; la 1.23.0 se livre après la
1.22.0 recettée sur ProdEssai (décision « deux livraisons » du 10 octobre), les trois migrations de la version s'appliquant au
démarrage dans l'ordre Lot13b_SocleRh_Nettoyage, Lot14_ComplementsRh, Lot14_JournalContratsIndex, suivies de l'amorçage
System.Upgrade.Lot14.
Ce que le lot apporte, en une page. Des référentiels RH du groupe (catégories, grades par société, qualifications,
collèges, types d'employé, motifs de sortie, natures de contrat), un classement daté des dossiers, des réglages par
société (Setting.CompanyId, repli global), des documents de voyage et de séjour typés avec alertes d'expiration, le
self-service de la fiche avec validation RH, des instantanés de fiche (historique et comparaison), des tâches à la
DSI pour les sociétés gérées par l'intranet, le circuit des contrats (relances paramétrables, demandes CONTRACT_RENEWAL /
CONTRACT_TRIAL, décisions appliquées ou renvoyées à IES) et le contrat d'import v3 porté par l'agent 1.2.0 (references,
documents, classements et motif codé sur employees, active sur contracts). Chiffres de la version : 84 DbSet
(77 → 84), 23 migrations (Lot1_Initial à Lot14_JournalContratsIndex), 2 615 clés par langue dans les quatre .resx
(2 447 après l'étape 2, 2 615 au terme du lot), 365 tests serveur et 286 tests de l'agent ; liste blanche erp de 27 tables et 101 colonnes, douze sentinelles,
dix routes d'import.
- Étape 2 :
Lot13b_SocleRh_Nettoyage(migration20261010233324_Lot13b_SocleRh_Nettoyage, séparée deLot14_ComplementsRh; s'applique après la recette de la 1.22.0 en production ;Downinterdit en production :docs/PROCEDURE-MISE-A-JOUR.md, 1.22.0, point 15).- Modèle.
Employee:PersonIdNOT NULL(toute relation d'emploi a une personne), suppression deUserIdet des 21 colonnes d'identité reprises parPerson(Civility…SocialSecurityProtected,PhotoFileId) ;BankAccountProtectedreste sur l'emploi.Assignment:EmployeeIdNOT NULL, suppression deUserIdet deManagerUserId;ManagerEmployeeIdest le seul N+1 d'une affectation.OrgUnit.ManagerUserIdconservé (responsable d'unité par compte, externes). Index :(PersonId, CompanyId)unique sans filtre ;(EmployeeId, ExternalCode)filtré surExternalCodeseul ; index et clés étrangères des colonnes supprimées retirés (4DropForeignKey, 7DropIndex, 24DropColumn, 2AlterColumn, 2CreateIndex). - Code. Le compte d'un dossier se lit partout par
Employee.Person.UserId(Include(Person).ThenInclude(User)ou projection ; jamais une requête par ligne) :UserContext.ExitedCompanyIdsAsync(coupure d'accès des sortis, sécurité), annuaire, recherche, verrou IES, services RH, demandes, compétences, fichiers, lecture d'intégration (contrat Badge / GMAO inchangé).OrganizationService.AssignAsyncn'écrit queManagerEmployeeId: un N+1 saisi par compte est converti en son dossier présent dans la société, un compte sans dossier est refusé (Error.ManagerEmployeeRequired) ; les unités gardent le repli par compte.PersonAccounts.MirrorAsyncne tient plus queOrgUnit.ManagerUser.Seed/Lot13Upgradesupprimé ;DbSeederpose la cléSystem.Upgrade.Lot13etSeedOrganisationAsyncrelie les affectations de reprise par la personne. DTO conservés (EmployeeDto.UserId,PhotoFileId, bloc identité,AssignmentDto.ManagerUserId= compte de l'employé responsable), alimentés depuisPerson: Web, Api, Shared, PWA et MAUI inchangés. - Garde-fous. Contrôles de données avant la mise à jour (conception § 3.4, section « Lot13b » de
deploy/Lot13-Recette-Comptages.ps1), rejoués par Feki en production après la recette de la 1.22.0 ; testsLot13bTests(coupure IES par la personne, sortie parEmployeeId, N+1 refusé, unité par compte, amorçage par la personne, modèle SQL Server, invariants sur jeu amorcé) ; une base qui saute la 1.22.0 échoue dans la migration (UPDATE … = 0des lignes nulles refusé par les clés étrangères versPeople/Employees; retour arrière = sauvegarde,docs/PROCEDURE-MISE-A-JOUR.mdpoint 15).
- Modèle.
- Étape 3 : réglages par société, référentiels RH, classement daté (migration
20261011002237_Lot14_ComplementsRh, additive, appliquée aprèsLot13b; amorçageSystem.Upgrade.Lot14au premier démarrage).- Réglages par société.
Setting.CompanyId(nul = valeur globale) ; index uniqueKeyseul remplacé par(CompanyId, Key)filtré[CompanyId] IS NOT NULLetKeyfiltré[CompanyId] IS NULL.SettingsService.GetAsync(key, companyId)lit la valeur de la société, puis la valeur globale, puis le défaut de la définition ;SetAsync(key, value, companyId)n'accepte une société que pour une clé déclaréePerCompany(Error.SettingNotPerCompany) ; une valeur vide pour une société supprime sa ligne (repli global) ; cache(CompanyId, Key). ClésPerCompany:Hr.DocumentAlertDays,Hr.DocumentAlertRecipients,Hr.SelfServiceFields,Hr.SelfServiceValidation,Hr.SnapshotRetentionMonths,Hr.DsiTaskAssignee;Attendance.*,Hr.LeaveDaysPerYear,Hr.HolidaysetHr.Weekendrestent globaux (leurs lecteurs lisent la valeur globale) et ne seront réglables par société que quand leurs lecteurs seront basculés, lots 16 à 18 ; les gardes*.LastRunetSystem.Upgrade.*restent globales. Écran Administration → Paramètres : onglets Global et Par société (chip « hérité » tant qu'aucune valeur propre n'est enregistrée). Les lecteurs existants passent par la valeur globale tant que la clé n'a pas été basculée sur la société du contexte (étapes 4 et lots 16 à 18). - Référentiels RH.
HrReference(tableHrReferences) : nomenclature du groupe (TenantCompanyId= société racine, commePerson), item propre à une société parCompanyId(grades d'IES par société) ;Kind:Category,Grade,Qualification,College,EmployeeType,ExitReason,ContractNature;Codemajuscules sans accent (HrReferenceCodes.Normalize: accents détachés, espaces et ponctuation en_, 64 caractères), unique par (groupe, genre, portée) grâce à deux index filtrés ;ExternalCode(« IES::
») unique par groupe ; LabeletLabelsJson(en, ar, pt) ;ValidFrom/ValidTo/SortOrder/IsActive. Un item fermé (ValidTo) n'est plus proposé mais reste lisible dans l'historique des dossiers ; aucune suppression. Un item importé d'IES (ExternalCode) est verrouillé (code et libellé) quand il est propre à une société gérée par IES ou quand son groupe compte une société gérée par IES ; fin de validité, ordre et traductions restent modifiables. ServiceHumanResources/HrReferenceService(ListAsync,SaveAsync,CloseAsync,ChoicesAsyncgroupe + société en vigueur,ResolveAsyncpour l'import et l'amorçage,ClassifyAsync,HistoryAsync) ; droithr.references.manage; vue Références du module RH (HrReferencesPanel, sans route propre).- Classement daté.
EmployeeClassification(tableEmployeeClassifications) :EmployeeId,Kind,HrReferenceId,StartDate,EndDate(nul = en vigueur),Source(« IES » = ligne importée, verrouillée),ExternalCode(« IES:CE»), Note. En vigueur =EndDatenul ou ≥ aujourd'hui ; une seule ligne en vigueur par (EmployeeId,Kind) (index unique filtré[EndDate] IS NULLet contrôle de l'amorçage) ;ClassifyAsync(hr.manage, société du dossier) clôt la veille la ligne en vigueur du même genre et refuse une ligne IES d'une société gérée (Error.ReferenceLocked).Employee.ExitReasonId(FKRestrictversHrReferences, genreExitReason) s'ajoute au texteExitReason, conservé ; la fiche montreExitReasonLabel ?? ExitReason. Bloc Classement de la fiche (lignes en vigueur, historique, bouton Classer) ;EmployeeDto.Classifications,ExitReasonId,ExitReasonLabel.- Tables posées pour les étapes 4 et 5 (aucune migration ensuite) :
EmployeeDocumentsenrichie (DocumentType,Status,CountryCode,Zone,MultipleEntries,IssuedOn,AlertDaysOverride,ExternalCode,FileIdfacultatif),EmployeeChangeRequests,EmployeeSnapshots(projectionEmployeeSnapshotContent: emploi et identité, jamais les champs chiffrés ni les numéros de pièces ; empreinte SHA-256),EmployeeDocumentAlerts,ContractReminderRules,ContractReminderLogs. Types de demande livrésCONTRACT_RENEWAL(N+1 puis RH) etCONTRACT_TRIAL(RH seule) dansRequestService.BuiltIn().- Amorçage
System.Upgrade.Lot14(Seed/Lot14Upgrade, une fois, transaction, rejouable après échec partiel : chaque étape teste l'existence) : motifs de sortie par groupe depuisEmployees.ExitReasonet résolution deEmployees.ExitReasonId; natures de contrat depuisEmploymentContract.Classification; typesCONTRACT_*et deux règles de relance (« 45, 32 » ; « 10, 7, 4 ») pour chaque société active ; un instantané « Lot14 » par dossier ; contrôle final (motifs non résolus, codes en double, classements multiples en vigueur, sociétés sans règles, dossiers sans instantané, sociétés sans types) : un échec annule tout et arrête le démarrage. Permissions (System.Upgrade.Lot14Permissions) :hr.references.manageethr.documentsaux rôles porteurs dehr.manage,hr.selfserviceà tous les rôles non externes (Hr.ExternalRoles).- Convention
Company.HrManagedBy(L14-Q18) : nul ou vide = société gérée par l'intranet ; « IES » (comparaison insensible à la casse) = gérée par IES (dossiers, items et classements importés verrouillés ; décisions de contrat renvoyées à IES par une tâche).- Tests
Lot14ComplementsRhTests(17) : réglages (repli, refus, cache), référentiels (fermé lisible, code normalisé, verrou IES, choix groupe + société), classements (une ligne en vigueur, clôture la veille), motif de sortie de la fiche, amorçage (rejeu, casse et accents, natures, types et règles, permissions, instantanés sans champ chiffré), modèle SQL Server (index filtrés,Restrict, aucundecimal).- Étape 4 : documents du dossier, alertes d'expiration, self-service, instantanés, tâches DSI, passage quotidien (aucune migration : tables et colonnes posées à l'étape 3).
- Documents.
EmployeeDocumentporte une pièce typée (DocumentTypepasseport, visa, titre de séjour, permis de travail, autre ;Categoryreste le classement du fichier), un statut stocké (Valid,Renewing,Cancelled;ExpiringetExpiredcalculés des dates à la lecture,EmployeeDocumentDto.DisplayStatus), pays ISO 2, zone, entrées simples / multiples, délivré le, seuils propres (AlertDaysOverride), clé d'import (ExternalCode« IES:P» / « IES:DS ») ; FileIdfacultatif (HasFile). Aucun numéro de pièce (M § 0.4). ServiceHumanResources/HrDocumentService(AddAsync: titulaire ouhr.documents;UpdateAsync,RemoveAsync:hr.documents;ListAsync) : un document importé d'IES dans une société gérée par IES est verrouillé (statut, seuils et libellé seuls modifiables, jamais supprimé :Error.DocumentImported) ; audithr.document.*à compteurs (type, catégorie, présence d'un fichier), jamais le libellé ni le pays. Onglet Documents de la fiche (DxGrid, saisie typée avec fichier facultatif, verrou visible). - Alertes d'expiration (
HrDocumentService.AlertPassAsync, appelée parDailyAsyncune fois par jour,Hr.DocumentAlertLastRun). Par société : seuilsHr.DocumentAlertDayspar type (défaut passeport 180 / 90 / 30 ; visa, séjour, permis 60 / 30 / 7 ; autre 30) ouAlertDaysOverridedu document ; documents non annulés d'un dossier présent ; seuil notifié = plus petit seuil ≥ jours restants (0 si expiré) ; une ligneEmployeeDocumentAlertspar document et seuil (index unique), jamais répétée, écrite même sans destinataire résolu ; destinatairesHr.DocumentAlertRecipients(employee,manager,hr) résolus parOrgHierarchy(titulaire, N+1, porteurs actifs dehr.manage) ; notificationsSourceApp = HR,ExternalId = doc:{id}:{seuil}(idempotentes),Hr.Notif.DocExpiring, avertissement si expiré. - Self-service de la fiche (
EmployeeService.UpdateMineAsync, droithr.selfservice, dossier du compte connecté dans sa société). Champs ouverts par société (Hr.SelfServiceFields: adresse, ville, pays, courriel et téléphone personnels, contact d'urgence, situation familiale, enfants, photo) ; les champs d'état civil importés d'IES (EmployeeService.ImportedSelfServiceFields: civilité, noms, naissance, nationalité) restent verrouillés sur un dossier géré, le téléphone personnel jamais importé reste saisi dans l'intranet (M § 0.10) ; un champ fermé ou importé modifié →Error.SelfServiceFieldLocked. AvecHr.SelfServiceValidation(défaut vrai) :EmployeeChangeRequest(FieldsJson= champ, avant, après ; une seule en attente par dossier, index filtré,Error.ChangeRequestPending), notificationHr.Notif.ChangeRequestedaux porteurs dehr.people, décisionDecideChangeAsync(hr.people: approuvée → valeurs écrites et instantané « Self-service » au nom du demandeur ; refusée → motif), notificationHr.Notif.ChangeDecided; sans validation : écriture immédiate, audithr.selfservice.update(fields=<nombre>). Écrans : bouton Modifier mes informations de Mon dossier (DxPopupaux champs du réglage), bandeau de la demande en attente, vue RH Demandes de modification (approuver / refuser avec motif) ; lien « Mon dossier RH » du profil. - Instantanés de fiche (
EmployeeSnapshot, projectionEmployeeSnapshotContent: emploi, identité, classements en vigueur ; jamais CIN, passeport, sécurité sociale, coordonnées bancaires, expiration du passeport, fichier de contrat). Pris après chaqueSaveAsync(« RH », compte de la RH), self-service accepté (« Self-service », compte du titulaire), import (« Import »), amorçage (« Lot14 ») ; empreinte SHA-256 : un contenu identique au précédent n'est pas réécrit.SnapshotsAsync,CompareSnapshotsAsync(champ en PascalCase, avant, après ;HrNotesmasqué pour le titulaire) ; onglet Historique de la fiche (RH et titulaire). PurgePurgeSnapshotsAsync(Hr.SnapshotRetentionMonthspar société, défaut 60 ; le dernier instantané de chaque dossier est toujours gardé), appelée parHrDocumentService.DailyAsync. - Tâches à la DSI (M § 0.3 : Domino, Active Directory, AX sans écriture). Pour une société gérée par l'intranet
(
HrManagedBynul ou vide),SaveAsyncouvre une tâcheHRidempotente (employee:{id}:dsi:created|leaving|reactivated,Hr.DsiTask.Title.*, liste de contrôleHr.DsiTask.Body, échéance +3 jours, lien/hr?employee=<id>) assignée selonHr.DsiTaskAssignee(login, ourole:<nom du rôle>= premier membre actif du rôle dans la société) ; aucune tâche sans assigné membre actif (avertissement journalisé) ; jamais pour une société gérée par IES ni depuis l'import. - Passage quotidien (
Web/Services/MailOutboxWorker, toutes les 15 minutes, une passe par jour chacune) : employés automatiques, alertes de certification, demandes, alertes de documents et purge des instantanés ; untry/catchpar passe, une passe en échec ne saute plus les suivantes. - Tests
Lot14ComplementsRhTests.Etape4.cs(13) : alertes (une fois par seuil, destinataires du réglage, document annulé), document importé (verrou, suppression refusée, droits du titulaire), self-service (champ importé et champ fermé, écriture auditée, demande acceptée puis refusée, une seule en attente), instantanés (pris et comparés, sans champ chiffré ni numéro, non réécrits, purgés), tâche DSI (société intranet seulement).
- Étape 5 : circuit des contrats (M § 0.6 et § 1.6, conception § 2.7, 2.8, 6.1, 6.4 ; une migration d'index
20261011063401_Lot14_JournalContratsIndex, tables posées à l'étape 3). ServiceHumanResources/ContractService(singleton,IDbContextFactory),IApprovalHandlerenregistré en DI sur la même instance et résolu parRequestServiceenIEnumerable(L14-Q17 : aucun cycle de dépendance,ContractServicene connaît pasRequestService).- Règles de relance
ContractReminderRule(une par société et genreContractEnd/TrialEnd: seuils « 45, 32 » et « 10, 7, 4 » par défaut, N+1 et RH informés, employé non, tâche créée) :RulesAsync(hr.contractsouhr.manage, défauts rendus pour un genre sans ligne),SaveRuleAsync(hr.contracts, liste normalisée,Error.DaysBeforeInvalid, audithr.contract.ruleà compteurs). Bloc Règles de relance de la vue Contrats du module RH (HrContractRulesPanel, sans route). - Passe de relances
ReminderPassAsync(appelée parDailyAsyncune fois par jour,Hr.ContractReminderLastRun, après les alertes de documents dansMailOutboxWorker). Par société aux règles actives : contrats des dossiers présents, sans contrat postérieur ni décision appliquée ; fin de contrat (non CDI,EndDatedu jour ou à venir) et fin d'essai (TrialEndDatedu jour ou à venir) ; une échéance passée n'est jamais relancée (anomalie signalée par l'import). Une ligneContractReminderLogspar seuil atteint (index unique(ContractId, Kind, DaysBefore)filtré sur les genres 1 et 2) ; notifications (destinataires de la règle ;SourceApp = HR,ExternalId = contract:{id}:end|trial:{seuil},Hr.Notif.ContractEnding/TrialEnding), tâcheHRidempotente au premier porteur actif dehr.manage(à défaut au N+1) siCreateTask, et demandeCONTRACT_RENEWAL/CONTRACT_TRIALau premier seuil franchi seulement (une par contrat) : brouillon créé directement en base au nom du N+1 utilisable (à défaut de la RH ; sans l'un ni l'autre, tâche seule),OnBehalfOfUserId= compte de l'employé,DataJson.contractId= contrat visé,NumberparRequestService.NextNumberAsync. Seuils atteints ensemble (premier passage) : toutes les lignes sont écrites, mais notifications, tâche et demande ne partent qu'une fois, au plus petit seuil (pas de rafale). Le demandeur du brouillon est toujours notifié ; le lien de la notification et de la tâche est la demande (/requests/{id}), sinon le dossier. - Demandes
CONTRACT_*(types livrés à l'étape 3 :CONTRACT_RENEWALN+1 puis RH,CONTRACT_TRIALRH seule). Le brouillon s'ouvre depuis la notification : le N+1 saisit la décision (decision: renouveler, laisser expirer, passer en CDI ;newEndDate;trialDecision: confirmer, prolonger, mettre fin ;newTrialEndDate) et dépose ; comme demandeur, son étape N+1 est sautée, la RH valide.RequestService.SaveDraftAsyncverrouillecontractId: la valeur enregistrée est conservée, celle du formulaire ignorée, aucune à la création manuelle (une demande liée à un contrat ne naît que par la passe ou par Décider). Bouton Décider de l'onglet Contrats (hr.contracts,OpenDecisionRequestAsync) : rend la demande en cours du contrat et du genre, sinon ouvre un brouillon au nom du gestionnaire (la ligne de relance qui portait une demande close le suit) ; refusError.ContractDecisionAppliedsi une décision est déjà appliquée. Journal des relances et décisions du contrat (RemindersAsync, une requête : genre, seuil, date, demande avec numéro et statut, tâche) affiché dans la fiche. - Décision appliquée (
ApplyDecisionAsync, crochetIApprovalHandler.OnApprovedAsyncà l'approbation finale et rejeuApplyApprovedDecisionsAsyncparDailyAsyncpour les demandesApprovedsans ligne de genre 3 / 4 : marqueur persistant, aucune exception du crochet ne perd une décision). Société gérée par l'intranet : renouveler → contrat courant clos à sonEndDate, contrat suivant (StartDate = EndDate + 1,EndDate = newEndDateou même durée,KindetClassificationrecopiés,Note= numéro) ; laisser expirer → note ; CDI → contrat suivantPermanent; essai : confirmer → note, prolonger →TrialEndDate, mettre fin →EndDatedu jour (la sortie du dossier reste à la RH). Société gérée par IES : aucune écriture (M § 0.6) : tâcheHRcontract:{id}:decision:{requestId}au premier porteur dehr.manageet notification à toute la RH « à saisir dans IES, référence <numéro> » ; le contrat revient par l'import. Dans les deux cas une ligne de genre 3 / 4 (RequestIdunique parmi les lignes de décision, index filtré par20261011063401_Lot14_JournalContratsIndex; la relance garde sa demande) et un audithr.contract.decisionà compteurs. Un contrat déjà décidé qui reçoit une seconde demande approuvée ne reçoit qu'un marqueur (la première décision reste, le rejeu ne boucle pas). - Tests
Lot14ComplementsRhTests.Etape5.cs(11 : 8 de la conception, 3 de la critique) : relance de contrat et d'essai une fois par seuil (exclusions, seuils simultanés, règle modifiée ou désactivée, journal de l'onglet, index unique, passage quotidien), relance sans N+1 à la RH (et société sans type actif), demande créée au premier seuil (verroucontractId, dépôt, Décider, annulation), décision société intranet (renouveler, CDI, essai prolongé, essai terminé), décision société IES (tâche, notification, aucune écriture), décision appliquée une fois (rejeu, marqueur, demande sans contrat),SaveTypeAsync; test des types livrés de l'étape 3 complété.
- Étape 6 : agent 1.2.0 et import v3 (M § 3, conception § 5 ; aucune migration ; contrat additif :
docs/INTEGRATION-ERP.md§ 17 bis, noticedocs/AGENT-ERP.md, liste blanchedocs/ERP-SCHEMA-RH.md§ 10). Un agent 1.1.0 reste accepté par la 1.23.0 ; l'agent 1.2.0 exige la 1.23.0 (404 surreferencesface à une 1.22.0 : cycle arrêté).- Routes et DTO (
Shared/Contracts/HrImportContracts.cs, miroirAgent/Relay/ImportContracts.cs, comparés par réflexion) :POST hr/import/references?dryRun=(groupe, lothr.references, 500 :HrImportReference—externalCode« IES::
», kind,code,label,companyExternalCodepour un grade,labels,sortOrder,isActive) ;POST hr/import/documents?companyExternalCode=&dryRun=(société, lothr.documents, 500 :HrImportDocument— type, statut, pays, zone, entrées, dates, libellé ; jamais un numéro) ;HrImportEmployee.Classifications[](HrImportClassification: genre, référence, code externe « IES:CE» pour une ligne datée, dates) et HrImportEmployee.ExitReasonExternalCode;HrImportContract.Active. Codes de rejet et d'avertissement v3 dansHrImportReasons(25). Dix routes d'import au total ; prérequis de l'agent :referencesaprèscompanies, avantpeopleetemployees;documentsaprèsemployees.- Référentiels importés (
HrImportService.References.cs) : rapprochement par code externe, puis par libellé normalisé (HrReferenceCodes.Normalize) d'un item sans code externe de même genre et même portée (adoption :reference_adopted; les motifs de sortie de l'amorçageSystem.Upgrade.Lot14reçoivent ainsi leur clé IES), sinon création (code_takensi le code proposé est pris) ; un item fermé (ValidTo) n'est ni rouvert ni modifié (reference_closed) ; un seulSaveChanges; rien n'est supprimé. L'item importé est verrouillé (code, libellé) quand sa portée est gérée par IES (étape 3).- Documents importés (
HrImportService.Documents.cs) : garde-fou serveur 400unexpected_fieldsur le JSON brut (toute propriété dont le nom contient « number » ou « numero », objets imbriqués compris) avant toute lecture en base ; dossier de la société de l'appel par code externe (employee_unknownà renvoyer,erp_not_mastersi non géré par IES) ;EmployeeDocumentde catégorieIdentity, sans fichier, déposé par le compte 0 à l'heure de l'import ; mise à jour du type, du statut, du pays, de la zone, des entrées et des dates, du libellé seulement s'il est reçu ;AlertDaysOverrideet le journalEmployeeDocumentAlertsjamais touchés ; jamais supprimé par l'import (M § 0.9), verrouillé tant que la société est gérée par IES (étape 4) ; dates avant 1900 ignorées (date_out_of_range),dates_invertedetcountry_code_suspectsignalés, jamais corrigés.- Classements importés (
HrImportService.Employees.cs) : lignes datées (Grade, « IES:CE») créées ou mises à jour telles que reçues, Source = IES; lignes non datées (Category,Qualification,College,EmployeeType) : la ligne en vigueur du genre est gardée si elle vise le même item, sinon close la veille et remplacée ; filet de l'index « une ligne ouverte par (dossier, genre) » ; rien n'est supprimé ;unknown_reference(à renvoyer) ignore la ligne et importe le reste du dossier ;classification_overlapsignalé, jamais corrigé.ExitReasonIdrésolu parexitReasonExternalCode(aucun repli par libellé). Instantané « Import » (EmployeeService.TakeSnapshotAsync,EmployeeServiceinjecté en paramètre facultatif deHrImportService) quand la fiche change.- Contrats (
HrImportService.Contracts.cs) :activelu tel quel ;contract_expired_active(actif à fin passée) etcontract_overlap(périodes croisées avec les contrats existants du dossier ou les lignes du lot) comptés dans les avertissements, jamais corrigés (M § 0.6) ; l'état intranet reste déduit des dates.- Agent 1.2.0 (
src/Softic.Intranet.Agent, version 1.2.0, format de configuration 2 inchangé) : liste blancheerpde 27 tables et 101 colonnes (ReadOnlyGuardTests.Whitelists["erp"]=@lecturesdedocs/SQL-LOGIN-LECTURE-ERP.sql, testScript_sql_egal_liste_blanche) ; douze sentinelles (ErpSource.ErpSentinels, L14-Q14) contrôlées à chaque cycle, repli L14-Q21 parSELECT COL_LENGTH(@table, @name)(seule lecture du catalogue admise) quandHAS_PERMS_BY_NAMErend 0 ; neuf requêtes nouvelles (une par table, jamaisCOUT_HEURE,ID_Echelon,Echelon, un numéro, un lieu de délivrance ni un téléphone : testAucune_colonne_de_numero_ni_telephone) ; règles puresGroupRules(codes externes, codes courtsCAT/GRD/QUAL/COL/TYP/MOT, correspondances de types et de statuts L14-Q11, classements par dossier, documents par société du dossier, anomalies de contrats) ; cycle :references(temps 4 bis) etdocuments(temps 9 bis) ;erp-check: compteurs du § 5.5 de la conception ; étatstate.jsonformat 2 enrichi des dictionnairesReferencesetDocuments. Dix routes dansRelay/IntranetClient.cs.- Tests : serveur
Lot14ComplementsRhTests.Etape6.cs(12 : référentiels deux foisunchanged, adoption par libellé, fermé non rouvert ; documents deux foisunchanged, numéro refusé, dossier inconnu à renvoyer, verrou et non-suppression ; classement daté et chevauchement, référence inconnue à renvoyer,ExitReasonId, instantané ; contratsexpired_activeetoverlap) ; agentErpSyncLot14Tests(6),ContractV3Tests(2),ReadOnlyGuardTests(+3),OptionsLockStateTests(+1),GroupRulesTests(+1, complément :classement_societe_sans_dossier). Suites : 365 et 286.- Complément de l'étape 6 (recette de l'agent sur les copies locales) : un grade daté dont la société (
EIF_RH_Categorie_Employe.IdSociete) n'a aucun dossier envoyé pour la personne, ou dont le catalogue (EIF_RH_GradeCategory.Id_Societe,ReferencePlan.GradeCompanies) n'est pas la société du dossier cible, n'est pas envoyé par l'agent (compteurclassement_societe_sans_dossier) : le serveur le refuserait (unknown_reference, règle « même portée » du référentiel), et la ligne reviendrait « à renvoyer » à chaque cycle.- Étape 7 : recette et documentation (aucune migration ;
Directory.Build.props1.23.0 ;docs/COMPTE-RENDU-LOT14.md, étape 7).- Recettes
ClaudeTestjouées (11 octobre 2026, instance localesoftic-web-claudetest, copies localesERP/INTRANET) : migrations appliquées au démarrage ; amorçageSystem.Upgrade.Lot14(8 référentiels, 3 motifs résolus, 56 types, 56 règles, 1 660 instantanés) ; passe quotidienne en situation (40 relances pour 20 contrats de 3 sociétés, sans demande ni tâche faute de compte utilisable) ; agent 1.2.0 (erp-check,dry-run,oncepar paliers puis ×2 : références 507 créées + 1 adoptée, 811 dossiers classés avec leur instantané « Import », 52 documents créés ; second passage sans écriture, 0 ligne à renvoyer). - Finitions de code (sans effet fonctionnel) :
EmployeeService.AddDocumentAsync(lot 5) retirée, ses appelants (tests des lots 5 et 13) basculés surHrDocumentService.AddAsync;HasSentinelposé sur les défautsEmployeeDocument.DocumentType/Status(avertissement EF 20601 au démarrage éteint, capture du modèle inchangée) ; en-têteHr.Contract.Statede la colonne d'état de la grille Contrats (clé nouvelle × 4 langues). - Script de recette
deploy/Lot13-Recette-Comptages.ps1, section « Lot 14 » : 70 contrôles (SELECTseulement, « non disponible (colonne absente) » avant la migration) sur les référentiels, classements, motifs de sortie, documents et alertes, instantanés, demandes de modification, règles et journal des contrats, réglages par société et typesCONTRACT_*. - Décision sur la
Notedu contrat importé (conception § 6.4, écart de l'étape 6 ; à confirmer par Feki) : en 1.23.0 l'import ne touche pas laNoted'un contrat importé d'IES ; la relance et la décision restent visibles par le journalContractReminderLogsde la fiche (demande liée, tâche). Si la référence de la demande doit figurer sur le contrat revenu d'IES, la règle recommandée est le rapprochement par dates (même dossier,StartDate=newEndDate+ 1 de la décision), dans un lot ultérieur ; IES ne porte aucune référence de demande. - Reste hors lot : réglables par société seulement avec la bascule de leurs lecteurs,
Attendance.*,Hr.LeaveDaysPerYear,Hr.Holidays,Hr.Weekend(lots 16 à 18) ;TimeEntry.EmployeeId(lot 15, liaison Badge) ;PUT api/v1/hr/me(PWA / MAUI).
Lot 15 : liaison Softic.Badge et pointage physique (v1.24.0)
Mission
docs/MISSION-LIAISON-SOFTIC-BADGE.mdv3.0 (décisions intangibles § 0 : l'intranet est la référence du pointage ; Badge ne pilote aucun appareil tant que ZKAccess est en place ; rien n'est supprimé ; agent de site en lecture seule), conceptiondocs/LOT15-CONCEPTION.md, suividocs/COMPTE-RENDU-LOT15.md. Lot terminé côté Claude le 11 octobre 2026 en méthode allégée (code inline, relecture avant chaque commit).- Modèle (migration
Lot15_LiaisonBadge, amorçageSystem.Upgrade.Lot15) :TimeEntryporté par le dossier (EmployeeId,UserIdfacultatif), colonnes des appareils (EmployeeNumber,ExternalIdunique,DeviceSerial,DeviceName,SiteCode,VerifyMode,Direction,Collector,IgnoredUtc),ClockKind.Pass,WorkSite.Code(neuf sites EPPM amorcés),AppRegistration.SsoRoleRules. - API d'intégration (
Api/Controllers/AttendanceIntegrationController.cs, portéeattendance, clientIntegrations:Badge) :POST attendance/punches(lots de 500, idempotence parexternalIdpuis événement physique, rattachement par matricule sur le groupe de la clé, non rattachés conservés,dryRun) etGET hr/employees(format Badge, sans numéro de carte) ; routePOST hr/import/punchesde l'agent ERP, même réception (Collector = ies). - Services (
Engine/Engagement/AttendancePunchService.cs) : réception, non rattachés (lier, ignorer, audités), rapprochement quotidien, état de la liaison ;AttendanceServicepar dossier (présence d'un jour avec appareils = premier et dernier passage) ;SsoService.RoleOf(revendicationroledu ticket par règles d'application). - Écrans : Pointage (source, appareil, sens, filtre, onglet Non rattachés), Intégrations (bloc Softic.Badge), Applications (Rôles SSO).
- Softic.Badge 1.1.0 (dépôt Badge) : chemins
api/integration/v1, réponse du lot ligne à ligne, synchronisation du personnel sans réécriture du PIN ni commande vers les appareils (D13), curseurupdatedSince, remise en file des pointages ignorés, SSO par ticket (/sso,/sso/status), agent de site : pointeuse DIT exclue,verifyMode2 ; migrationLot15Liaisonà générer par Feki. - Agent ERP 1.3.0 : route
punches(badgeages d'IES par pages d'identifiants, dédoublonnage, sens parRH_Ports_Pointeuses.Type, étatPunchExport,once --routes punchespour l'historique 2024-09-01 → 2026-09-01, 1 203 lots),erp-check« Badgeages IES ». - Tests : intranet 384, agent 291, Badge 148 ; recettes
ClaudeTest(migration, API parcurl, écrans, agent 1.3.0 sur un mois réel : 28 195 pointages, 1 903 non rattachés, 0 rejet). - Reste à Feki : fenêtre de déploiement de Badge 1.1.0 (L15-Q13), sites des filiales et couverture (L15-Q14), migration Badge, liste blanche
Typechez EPPM, client et applicationBADGE, paquets ; sens des tourniquets (P17), alimentation d'IES après ZKAccess (P19).
- Référentiels importés (
- Classement daté.
- Réglages par société.