Files
zentral/PLAN.md
2025-11-28 18:03:34 +01:00

12 KiB

Piano: Collaborazione Real-Time nel Report Designer

Obiettivo

Implementare la collaborazione in tempo reale nel Report Designer, simile a Google Docs o Excel Online. Quando un utente modifica un template, tutti gli altri utenti collegati allo stesso template vedono le modifiche istantaneamente.


Architettura Proposta

Concetti Chiave

  1. Room-based Collaboration: Ogni template ha una "room" SignalR dedicata
  2. Operational Transformation Semplificata: Invece di OT completo (complesso), usiamo un modello "last-write-wins" con sync frequente
  3. Presence Awareness: Gli utenti vedono chi altro sta modificando il template
  4. Cursor/Selection Sharing: Mostra quale elemento sta selezionando ogni utente

Flusso Dati

User A modifica elemento
       ↓
Template locale aggiornato (come ora)
       ↓
SignalR invia delta alla room
       ↓
Backend riceve e ritrasmette a tutti nella room (eccetto mittente)
       ↓
User B/C/... ricevono delta
       ↓
Applicano delta al loro template locale
       ↓
Canvas si aggiorna automaticamente (già funziona così)

Implementazione Dettagliata

FASE 1: Backend - ReportCollaborationHub

File: /src/Apollinare.API/Hubs/ReportCollaborationHub.cs

Nuovo Hub dedicato per la collaborazione sui report:

public class ReportCollaborationHub : Hub
{
    // Stato in-memory degli utenti per template
    private static ConcurrentDictionary<string, HashSet<CollaboratorInfo>> _templateUsers = new();

    // Join a template room
    public async Task JoinTemplate(int templateId, string userName, string userColor)

    // Leave template room
    public async Task LeaveTemplate(int templateId)

    // Broadcast element change to room
    public async Task ElementChanged(int templateId, ElementChangeDto change)

    // Broadcast element added
    public async Task ElementAdded(int templateId, AprtElement element)

    // Broadcast element deleted
    public async Task ElementDeleted(int templateId, string elementId)

    // Broadcast page changes
    public async Task PageChanged(int templateId, PageChangeDto change)

    // Broadcast selection change (which element user is editing)
    public async Task SelectionChanged(int templateId, string? elementId)

    // Broadcast cursor position (optional, for live cursors)
    public async Task CursorMoved(int templateId, float x, float y)

    // Request full template sync (when joining late or after reconnect)
    public async Task RequestSync(int templateId)

    // Send full template state (host responds to sync requests)
    public async Task SendSync(int templateId, string connectionId, AprtTemplate template)
}

DTOs:

public record CollaboratorInfo(string ConnectionId, string UserName, string Color, string? SelectedElementId);

public record ElementChangeDto(
    string ElementId,
    string ChangeType,  // "position", "style", "content", "visibility", etc.
    object NewValue
);

public record PageChangeDto(
    string PageId,
    string ChangeType,  // "added", "deleted", "reordered", "renamed", "settings"
    object? Data
);

FASE 2: Frontend - Servizio Collaborazione

File: /frontend/src/services/reportCollaboration.ts

class ReportCollaborationService {
  private connection: HubConnection | null = null;
  private templateId: number | null = null;
  private listeners: Map<string, Set<Function>> = new Map();

  // Connessione e gestione room
  async joinTemplate(
    templateId: number,
    userName: string,
    userColor: string,
  ): Promise<void>;
  async leaveTemplate(): Promise<void>;

  // Invio modifiche (chiamati dal ReportEditorPage)
  sendElementChange(elementId: string, changeType: string, newValue: any): void;
  sendElementAdded(element: AprtElement): void;
  sendElementDeleted(elementId: string): void;
  sendPageChange(pageId: string, changeType: string, data?: any): void;
  sendSelectionChange(elementId: string | null): void;

  // Sottoscrizione eventi (per ricevere modifiche da altri)
  onElementChanged(callback: (change: ElementChange) => void): () => void;
  onElementAdded(callback: (element: AprtElement) => void): () => void;
  onElementDeleted(callback: (elementId: string) => void): () => void;
  onPageChanged(callback: (change: PageChange) => void): () => void;
  onCollaboratorsChanged(
    callback: (collaborators: Collaborator[]) => void,
  ): () => void;
  onSelectionChanged(
    callback: (userId: string, elementId: string | null) => void,
  ): () => void;
  onSyncRequested(callback: (requesterId: string) => void): () => void;

  // Sync
  requestSync(): void;
  sendSync(connectionId: string, template: AprtTemplate): void;
}

FASE 3: Frontend - Integrazione in ReportEditorPage

Modifiche a: /frontend/src/pages/ReportEditorPage.tsx

  1. Nuovo State per collaborazione:
const [collaborators, setCollaborators] = useState<Collaborator[]>([]);
const [remoteSelections, setRemoteSelections] = useState<Map<string, string>>(
  new Map(),
);
const [isCollaborating, setIsCollaborating] = useState(false);
  1. Join/Leave room al mount/unmount:
useEffect(() => {
  if (!isNew && id) {
    const userName = getCurrentUserName(); // Da auth context
    const userColor = generateUserColor(userName);

    reportCollaborationService
      .joinTemplate(Number(id), userName, userColor)
      .then(() => setIsCollaborating(true));

    return () => {
      reportCollaborationService.leaveTemplate();
    };
  }
}, [id, isNew]);
  1. Sottoscrizione eventi remoti:
useEffect(() => {
  if (!isCollaborating) return;

  const unsubscribers = [
    reportCollaborationService.onElementChanged((change) => {
      // Applica modifica senza creare history entry
      historyActions.setWithoutHistory((prev) => {
        // ... applica change.newValue a element con change.elementId
      });
    }),

    reportCollaborationService.onElementAdded((element) => {
      historyActions.setWithoutHistory((prev) => ({
        ...prev,
        elements: [...prev.elements, element],
      }));
    }),

    // ... altri handler

    reportCollaborationService.onCollaboratorsChanged(setCollaborators),
  ];

  return () => unsubscribers.forEach((unsub) => unsub());
}, [isCollaborating]);
  1. Invio modifiche locali:

Modificare handleUpdateElement per inviare anche via SignalR:

const handleUpdateElement = useCallback(
  (elementId: string, updates: Partial<AprtElement>) => {
    // Aggiorna stato locale (come ora)
    historyActions.set((prev) => ({...}));

    // Invia a collaboratori
    if (isCollaborating) {
      reportCollaborationService.sendElementChange(elementId, "update", updates);
    }
  },
  [historyActions, isCollaborating],
);

FASE 4: UI Collaborazione

File: /frontend/src/components/reportEditor/CollaboratorsBar.tsx

Barra che mostra gli utenti connessi:

interface CollaboratorsBarProps {
  collaborators: Collaborator[];
  remoteSelections: Map<string, string>; // userId -> elementId
}

// Mostra:
// - Avatar circolari colorati per ogni collaboratore
// - Tooltip con nome utente
// - Indicatore "sta modificando [elemento]"
// - Badge con conteggio totale collaboratori

Modifiche a EditorCanvas: Evidenziare elementi selezionati da altri utenti con bordo colorato.

FASE 5: Gestione Conflitti

Per semplicità, usiamo strategia "last-write-wins" con alcune ottimizzazioni:

  1. Elementi diversi: Nessun conflitto, modifiche applicate indipendentemente
  2. Stesso elemento, proprietà diverse: Merge delle proprietà
  3. Stesso elemento, stessa proprietà: Ultima modifica vince
  4. Lock visivo: Quando un utente seleziona un elemento, gli altri vedono un indicatore

Opzionale (Fase futura): Lock pessimistico - solo un utente alla volta può modificare un elemento.


File da Creare/Modificare

Nuovi File

File Descrizione
src/Apollinare.API/Hubs/ReportCollaborationHub.cs Hub SignalR per collaborazione
src/Apollinare.API/Models/CollaborationDtos.cs DTOs per messaggi collaborazione
frontend/src/services/reportCollaboration.ts Client SignalR per collaborazione
frontend/src/components/reportEditor/CollaboratorsBar.tsx UI collaboratori connessi
frontend/src/types/collaboration.ts Types TypeScript per collaborazione

File da Modificare

File Modifiche
src/Apollinare.API/Program.cs Registrare nuovo hub /hubs/report-collaboration
frontend/src/pages/ReportEditorPage.tsx Integrare collaborazione, stato collaboratori
frontend/src/components/reportEditor/EditorCanvas.tsx Mostrare selezioni remote
frontend/src/components/reportEditor/EditorToolbar.tsx Mostrare CollaboratorsBar

Stima Complessità

Fase Complessità Note
1. Backend Hub Media SignalR groups, gestione stato in-memory
2. Frontend Service Media Gestione connessione, eventi
3. Integrazione Editor Alta Molti handler da modificare
4. UI Collaboratori Bassa Componente semplice
5. Gestione Conflitti Media Merge logic

Considerazioni Aggiuntive

Performance

  • Throttling degli aggiornamenti durante drag (ogni 50-100ms invece di ogni frame)
  • Debounce per modifiche testo (300ms)
  • Batch di modifiche multiple in singolo messaggio

Scalabilità

  • Per deployment multi-server: usare Redis backplane per SignalR
  • Considerare Azure SignalR Service per produzione

Autenticazione

  • Aggiungere autenticazione al hub (verificare che utente abbia accesso al template)
  • Usare JWT token per identificare utente

Edge Cases

  • Reconnessione dopo disconnessione: richiedere sync completo
  • Template eliminato mentre utenti connessi: notificare e chiudere
  • Conflitto salvataggio: merge o "force save" con conferma

Ordine di Implementazione Consigliato

  1. Backend Hub base - Join/Leave room, broadcast semplice
  2. Frontend service base - Connessione, invio/ricezione messaggi
  3. Integrazione minima - Solo sync modifiche elementi (no UI collaboratori)
  4. Test funzionale - Verificare che modifiche si propaghino
  5. UI Collaboratori - Mostrare chi è connesso
  6. Selezioni remote - Evidenziare elementi selezionati da altri
  7. Ottimizzazioni - Throttling, batching, gestione conflitti

Domande per l'Utente

Prima di procedere, confermare:

  1. Autenticazione: Il sistema ha già autenticazione utenti? Devo usare un sistema mock per ora?
  2. Persistenza stato: Le modifiche devono essere salvate automaticamente o solo quando l'utente clicca "Salva"?
  3. Lock elementi: Vuoi che solo un utente alla volta possa modificare un elemento (lock pessimistico)?
  4. Cursori live: Vuoi vedere il cursore degli altri utenti in tempo reale (come Google Docs)?
  5. Nome utente: Da dove prendo il nome utente da mostrare? (localStorage, auth context, prompt?)