feat(sprint-15 web 2026-04-25): Live Activity Feed page (transparency operations)
Page consommant /api/audit-trail/recent avec auto-refresh PrimeFaces toutes les 10s. Transparency opérationnelle UnionFlow — chaque utilisateur voit selon son scope. REST client AuditTrailRestClient - Nouvelle méthode recent(scope, orgId, userId, limit) Bean LiveFeedBean (@ViewScoped) - Polling pilote externe via p:poll interval=10 - 3 scopes : SELF (défaut, n'importe quel rôle), ORG (admin/officer), ALL (compliance/contrôleur) - Helpers : couleurAction (8 mappings), couleurSod (3 cas), tempsRelatif (s/m/h/j/futur) - Limit clamp [1, 500] - Compteur de refresh visible dans l'UI (debug) Page /pages/secure/conformite/live-feed.xhtml - Panel scope avec selectOneMenu + p:ajax change → rafraîchir - Conditional inputs : orgId si scope=ORG, userId si scope=SELF - p:poll interval=10 listener=rafraichir autoStart=true - DataTable opérations : index, "il y a Xs", acteur+rôle, action coloré, entité, description, SoD tag - Tag refresh counter (visible feedback) Centralisation - ViewPaths.CONFORMITE_LIVE_FEED + getter ViewPathsBean - menu.xhtml : entrée Live Feed sous sous-menu Conformité (icon pi-bolt) Tests (13/13 verts) - couleurAction × 4 (danger/success/info/autres) - couleurSod - tempsRelatif × 6 (null, secondes, minutes, heures, jours, futur) - setLimit clamp × 4 - defaults
This commit is contained in:
@@ -115,6 +115,7 @@ public final class ViewPaths {
|
||||
public static final String CONFORMITE_RAPPORTS_TRIMESTRIELS = "/pages/secure/conformite/rapports-trimestriels";
|
||||
public static final String CONFORMITE_BENEFICIAIRES_EFFECTIFS = "/pages/secure/conformite/beneficiaires-effectifs";
|
||||
public static final String CONFORMITE_AUDIT_TRAIL = "/pages/secure/conformite/audit-trail";
|
||||
public static final String CONFORMITE_LIVE_FEED = "/pages/secure/conformite/live-feed";
|
||||
|
||||
// ─── Admin technique ─────────────────────────────────────────────────────
|
||||
public static final String ADMIN_PISPI_READINESS = "/pages/secure/admin/pispi-readiness";
|
||||
|
||||
@@ -50,4 +50,13 @@ public interface AuditTrailRestClient {
|
||||
@PathParam("orgId") UUID orgId,
|
||||
@QueryParam("from") String from,
|
||||
@QueryParam("to") String to);
|
||||
|
||||
/** Live Activity Feed (Sprint 15) — N opérations les plus récentes selon scope. */
|
||||
@GET
|
||||
@Path("/recent")
|
||||
List<AuditTrailOperationResponse> recent(
|
||||
@QueryParam("scope") String scope,
|
||||
@QueryParam("orgId") UUID orgId,
|
||||
@QueryParam("userId") UUID userId,
|
||||
@QueryParam("limit") int limit);
|
||||
}
|
||||
|
||||
104
src/main/java/dev/lions/unionflow/client/view/LiveFeedBean.java
Normal file
104
src/main/java/dev/lions/unionflow/client/view/LiveFeedBean.java
Normal file
@@ -0,0 +1,104 @@
|
||||
package dev.lions.unionflow.client.view;
|
||||
|
||||
import dev.lions.unionflow.client.service.AuditTrailRestClient;
|
||||
import dev.lions.unionflow.server.api.dto.audit.response.AuditTrailOperationResponse;
|
||||
import jakarta.faces.application.FacesMessage;
|
||||
import jakarta.faces.context.FacesContext;
|
||||
import jakarta.faces.view.ViewScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Bean Live Activity Feed (Sprint 15 — transparency opérationnelle).
|
||||
*
|
||||
* <p>Polling 10s sur {@code /api/audit-trail/recent} avec choix du scope :
|
||||
* <ul>
|
||||
* <li>SELF — opérations de l'utilisateur courant (défaut)</li>
|
||||
* <li>ORG — opérations de l'organisation active</li>
|
||||
* <li>ALL — toutes opérations (compliance/contrôleur uniquement)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>L'auto-refresh est piloté côté UI via {@code <p:poll>} qui appelle {@link #rafraichir()}.
|
||||
*/
|
||||
@Named
|
||||
@ViewScoped
|
||||
public class LiveFeedBean implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Logger LOG = Logger.getLogger(LiveFeedBean.class);
|
||||
|
||||
@Inject @RestClient AuditTrailRestClient client;
|
||||
|
||||
/** SELF (défaut, n'importe quel rôle), ORG (admin/officer), ALL (compliance/contrôleur). */
|
||||
private String scope = "SELF";
|
||||
private UUID orgId;
|
||||
private UUID userId;
|
||||
private int limit = 50;
|
||||
|
||||
private List<AuditTrailOperationResponse> operations = Collections.emptyList();
|
||||
private long compteur;
|
||||
private String erreur;
|
||||
|
||||
public void rafraichir() {
|
||||
erreur = null;
|
||||
try {
|
||||
operations = client.recent(scope, orgId, userId, limit);
|
||||
compteur++;
|
||||
LOG.debugf("LiveFeed (%s) refresh #%d : %d ops", scope, compteur,
|
||||
operations == null ? 0 : operations.size());
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("LiveFeed refresh échoué : %s", e.getMessage());
|
||||
erreur = "Échec rafraîchissement : " + e.getMessage();
|
||||
FacesContext.getCurrentInstance().addMessage(null,
|
||||
new FacesMessage(FacesMessage.SEVERITY_WARN, "Erreur LiveFeed", erreur));
|
||||
operations = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public String getCouleurAction(String actionType) {
|
||||
if (actionType == null) return "secondary";
|
||||
return switch (actionType) {
|
||||
case "DELETE", "PAYMENT_FAILED" -> "danger";
|
||||
case "VALIDATE", "PAYMENT_CONFIRMED", "AID_REQUEST_APPROVED" -> "success";
|
||||
case "UPDATE", "PAYMENT_INITIATED", "BUDGET_APPROVED" -> "info";
|
||||
case "CREATE" -> "primary";
|
||||
case "EXPORT" -> "warning";
|
||||
default -> "secondary";
|
||||
};
|
||||
}
|
||||
|
||||
public String getCouleurSod(Boolean sodCheckPassed) {
|
||||
if (sodCheckPassed == null) return "secondary";
|
||||
return sodCheckPassed ? "success" : "danger";
|
||||
}
|
||||
|
||||
/** Pour l'affichage relatif "il y a Xs". */
|
||||
public String tempsRelatif(java.time.LocalDateTime dt) {
|
||||
if (dt == null) return "—";
|
||||
long secondes = java.time.Duration.between(dt, java.time.LocalDateTime.now()).getSeconds();
|
||||
if (secondes < 0) return "à l'instant";
|
||||
if (secondes < 60) return "il y a " + secondes + "s";
|
||||
if (secondes < 3600) return "il y a " + (secondes / 60) + "m";
|
||||
if (secondes < 86400) return "il y a " + (secondes / 3600) + "h";
|
||||
return "il y a " + (secondes / 86400) + "j";
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
public String getScope() { return scope; }
|
||||
public void setScope(String scope) { this.scope = scope; }
|
||||
public UUID getOrgId() { return orgId; }
|
||||
public void setOrgId(UUID orgId) { this.orgId = orgId; }
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
public int getLimit() { return limit; }
|
||||
public void setLimit(int limit) { this.limit = Math.max(1, Math.min(limit, 500)); }
|
||||
public List<AuditTrailOperationResponse> getOperations() { return operations; }
|
||||
public long getCompteur() { return compteur; }
|
||||
public String getErreur() { return erreur; }
|
||||
}
|
||||
@@ -110,6 +110,7 @@ public class ViewPathsBean implements Serializable {
|
||||
public String getConformiteRapportsTrimestriels() { return ViewPaths.CONFORMITE_RAPPORTS_TRIMESTRIELS; }
|
||||
public String getConformiteBeneficiairesEffectifs() { return ViewPaths.CONFORMITE_BENEFICIAIRES_EFFECTIFS; }
|
||||
public String getConformiteAuditTrail() { return ViewPaths.CONFORMITE_AUDIT_TRAIL; }
|
||||
public String getConformiteLiveFeed() { return ViewPaths.CONFORMITE_LIVE_FEED; }
|
||||
|
||||
// ─── Admin technique ─────────────────────────────────────────────────────
|
||||
public String getAdminPispiReadiness() { return ViewPaths.ADMIN_PISPI_READINESS; }
|
||||
|
||||
Reference in New Issue
Block a user