-
This commit is contained in:
66
CLAUDE.md
66
CLAUDE.md
@@ -46,13 +46,37 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve
|
|||||||
|
|
||||||
## Quick Start - Session Recovery
|
## Quick Start - Session Recovery
|
||||||
|
|
||||||
**Ultima sessione:** 28 Novembre 2025 (pomeriggio - sera)
|
**Ultima sessione:** 28 Novembre 2025 (notte)
|
||||||
|
|
||||||
**Stato progetto:** Migrazione Oracle APEX → .NET + React TypeScript in corso
|
**Stato progetto:** Migrazione Oracle APEX → .NET + React TypeScript in corso
|
||||||
|
|
||||||
**Lavoro completato nell'ultima sessione:**
|
**Lavoro completato nell'ultima sessione:**
|
||||||
|
|
||||||
- **NUOVA FEATURE: Sistema Collaborazione Real-Time Globale** - IN CORSO
|
- **NUOVA FEATURE: Sincronizzazione Real-Time Efficiente** - COMPLETATO
|
||||||
|
- **Prima:** Al salvataggio veniva inviata solo una notifica `DataSaved`, l'altra sessione ricaricava il template dal server (lento)
|
||||||
|
- **Dopo:** Al salvataggio viene inviato l'intero template via SignalR (`BroadcastTemplateSync`), l'altra sessione lo applica direttamente (istantaneo)
|
||||||
|
- **Compressione automatica** per template > 10KB usando gzip via browser's CompressionStream API
|
||||||
|
- **Version tracking** per gestione conflitti (ignora template con versione più vecchia)
|
||||||
|
- Nuovo metodo Hub: `BroadcastTemplateSync(roomKey, templateJson, version, compressed)`
|
||||||
|
- Nuovo evento: `TemplateSync` con decompressione automatica lato client
|
||||||
|
- **FIX: Limite messaggio SignalR** - Aumentato `MaximumReceiveMessageSize` a 1MB in `Program.cs` (default era 32KB)
|
||||||
|
- **File modificati:**
|
||||||
|
- `CollaborationHub.cs` - Aggiunto `BroadcastTemplateSync` e `TemplateSyncMessage` con campo `Compressed`
|
||||||
|
- `collaboration.ts` - Aggiunto `broadcastTemplateSync()`, utilities compressione/decompressione (`compressString`, `decompressString`), handler `TemplateSync`
|
||||||
|
- `CollaborationContext.tsx` - Esposto `broadcastTemplateSync` e `onTemplateSync`
|
||||||
|
- `ReportEditorPage.tsx` - Sostituito `sendDataSaved()` con `broadcastTemplateSync()`, aggiunto handler per applicare template ricevuti
|
||||||
|
- `Program.cs` - Configurato SignalR con `MaximumReceiveMessageSize = 1MB`
|
||||||
|
|
||||||
|
- **FIX: Auto-Save Event-Based con Debounce** - COMPLETATO
|
||||||
|
- Riscritto auto-save per triggerare ad ogni modifica (non a intervalli)
|
||||||
|
- Debounce di 500ms per evitare salvataggi multipli durante editing rapido
|
||||||
|
- Usa `useRef` per `saveMutation`, `template`, `templateInfo` per evitare re-creazione del timeout
|
||||||
|
- Dipendenze effect: solo `autoSaveEnabled`, `isNew`, `hasUnsavedChanges`, `templateHistory.undoCount`
|
||||||
|
- Il check `isPending` avviene al momento dell'esecuzione del timeout, non come dipendenza
|
||||||
|
|
||||||
|
**Lavoro completato nelle sessioni precedenti (28 Novembre 2025 pomeriggio - sera):**
|
||||||
|
|
||||||
|
- **NUOVA FEATURE: Sistema Collaborazione Real-Time Globale** - COMPLETATO
|
||||||
- Migrato da sistema report-specific a sistema globale per tutta l'app
|
- Migrato da sistema report-specific a sistema globale per tutta l'app
|
||||||
- `CollaborationHub.cs` - Hub SignalR generico per qualsiasi entità/pagina
|
- `CollaborationHub.cs` - Hub SignalR generico per qualsiasi entità/pagina
|
||||||
- `collaboration.ts` - Service singleton per gestione connessione
|
- `collaboration.ts` - Service singleton per gestione connessione
|
||||||
@@ -61,7 +85,6 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve
|
|||||||
- Room-based collaboration con formato `{entityType}:{entityId}`
|
- Room-based collaboration con formato `{entityType}:{entityId}`
|
||||||
- **FIX: Incompatibilità versione SignalR** - Rimosso package `Microsoft.AspNetCore.SignalR.Common` v10.0.0 incompatibile con .NET 9, downgrade client `@microsoft/signalr` a v8.0.7
|
- **FIX: Incompatibilità versione SignalR** - Rimosso package `Microsoft.AspNetCore.SignalR.Common` v10.0.0 incompatibile con .NET 9, downgrade client `@microsoft/signalr` a v8.0.7
|
||||||
- **FIX: Auto-save non funzionante** - Usati `useRef` per evitare re-run dell'effect causato da `saveMutation` nelle dependencies
|
- **FIX: Auto-save non funzionante** - Usati `useRef` per evitare re-run dell'effect causato da `saveMutation` nelle dependencies
|
||||||
- **IN DEBUG:** Sincronizzazione real-time tra sessioni - Il salvataggio manuale sincronizza, l'auto-save invia la notifica ma la sessione 2 non la riceve ancora
|
|
||||||
|
|
||||||
**Lavoro completato nelle sessioni precedenti (28 Novembre 2025 tarda notte):**
|
**Lavoro completato nelle sessioni precedenti (28 Novembre 2025 tarda notte):**
|
||||||
|
|
||||||
@@ -1077,22 +1100,47 @@ frontend/src/
|
|||||||
- **Soluzione:** Usati `useRef` per `saveMutation`, `template`, e `templateInfo` per evitare che l'effect si ri-esegua inutilmente
|
- **Soluzione:** Usati `useRef` per `saveMutation`, `template`, e `templateInfo` per evitare che l'effect si ri-esegua inutilmente
|
||||||
- **File:** `ReportEditorPage.tsx`
|
- **File:** `ReportEditorPage.tsx`
|
||||||
|
|
||||||
22. **Sistema Collaborazione Real-Time (IN CORSO 28/11/2025):**
|
22. **Sistema Collaborazione Real-Time (COMPLETATO 28/11/2025):**
|
||||||
- **Obiettivo:** Collaborazione stile Google Docs su tutto l'applicativo
|
- **Obiettivo:** Collaborazione stile Google Docs su tutto l'applicativo
|
||||||
- **Architettura implementata:**
|
- **Architettura implementata:**
|
||||||
- `CollaborationHub.cs` - Hub SignalR generico con room-based collaboration
|
- `CollaborationHub.cs` - Hub SignalR generico con room-based collaboration
|
||||||
- `collaboration.ts` - Service singleton frontend
|
- `collaboration.ts` - Service singleton frontend
|
||||||
- `CollaborationContext.tsx` - React Context con `useCollaborationRoom` hook
|
- `CollaborationContext.tsx` - React Context con `useCollaborationRoom` hook
|
||||||
- Room key format: `{entityType}:{entityId}` (es. `report-template:2`)
|
- Room key format: `{entityType}:{entityId}` (es. `report-template:2`)
|
||||||
- **Stato attuale:**
|
|
||||||
- Connessione e join room funzionanti
|
|
||||||
- Salvataggio manuale notifica le altre sessioni correttamente
|
|
||||||
- Auto-save invia `sendDataSaved()` ma le altre sessioni non ricevono la notifica
|
|
||||||
- **Debug in corso:** Aggiunto logging dettagliato per tracciare il flusso dei messaggi
|
|
||||||
- **File principali:**
|
- **File principali:**
|
||||||
- Backend: `CollaborationHub.cs`
|
- Backend: `CollaborationHub.cs`
|
||||||
- Frontend: `collaboration.ts`, `CollaborationContext.tsx`, `ReportEditorPage.tsx`
|
- Frontend: `collaboration.ts`, `CollaborationContext.tsx`, `ReportEditorPage.tsx`
|
||||||
|
|
||||||
|
23. **Sincronizzazione Real-Time Lenta (FIX 28/11/2025 sera):**
|
||||||
|
- **Problema:** Al salvataggio (manuale o auto-save), l'altra sessione impiegava diversi secondi per vedere le modifiche
|
||||||
|
- **Causa:** Il sistema usava `DataSaved` notification che causava un reload del template dal server (`queryClient.invalidateQueries`)
|
||||||
|
- **Soluzione:** Implementato `BroadcastTemplateSync` che invia l'intero template via SignalR direttamente alle altre sessioni:
|
||||||
|
- Nuovo metodo Hub `BroadcastTemplateSync(roomKey, templateJson, version, compressed)`
|
||||||
|
- L'altra sessione riceve il template e lo applica istantaneamente con `historyActions.setWithoutHistory()`
|
||||||
|
- Aggiunto version tracking per evitare di applicare versioni più vecchie
|
||||||
|
- Compressione automatica gzip per template > 10KB (usa browser's CompressionStream API)
|
||||||
|
- **File:** `CollaborationHub.cs`, `collaboration.ts`, `CollaborationContext.tsx`, `ReportEditorPage.tsx`
|
||||||
|
|
||||||
|
24. **Limite Messaggio SignalR (FIX 28/11/2025 notte):**
|
||||||
|
- **Problema:** La connessione SignalR si disconnetteva con errore "The maximum message size of 32768B was exceeded"
|
||||||
|
- **Causa:** Il template compresso (~110KB) superava il limite di default di SignalR (32KB)
|
||||||
|
- **Soluzione:** Configurato `MaximumReceiveMessageSize = 1MB` in `Program.cs`:
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddSignalR(options => {
|
||||||
|
options.MaximumReceiveMessageSize = 1024 * 1024; // 1MB
|
||||||
|
})
|
||||||
|
```
|
||||||
|
- **File:** `Program.cs`
|
||||||
|
|
||||||
|
25. **Auto-Save Non Funzionante (FIX 28/11/2025 notte):**
|
||||||
|
- **Problema:** L'auto-save non si attivava anche con modifiche non salvate
|
||||||
|
- **Causa:** Le dipendenze dell'effect includevano `saveMutation`, `template`, `templateInfo` che cambiano ad ogni render, causando reset continui del timeout
|
||||||
|
- **Soluzione:** Approccio event-based con debounce usando refs:
|
||||||
|
- `saveMutationRef`, `templateForSaveRef`, `templateInfoForSaveRef` per accedere ai valori correnti senza re-triggerare l'effect
|
||||||
|
- Dipendenze effect ridotte a: `autoSaveEnabled`, `isNew`, `hasUnsavedChanges`, `templateHistory.undoCount`
|
||||||
|
- Check `isPending` spostato dentro il callback del setTimeout
|
||||||
|
- **File:** `ReportEditorPage.tsx`
|
||||||
|
|
||||||
### Schema Database Report System
|
### Schema Database Report System
|
||||||
|
|
||||||
Le tabelle sono già nel DbContext (`AppollinareDbContext.cs`):
|
Le tabelle sono già nel DbContext (`AppollinareDbContext.cs`):
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
type SyncRequestMessage,
|
type SyncRequestMessage,
|
||||||
type SyncDataMessage,
|
type SyncDataMessage,
|
||||||
type DataSavedMessage,
|
type DataSavedMessage,
|
||||||
|
type TemplateSyncMessage,
|
||||||
type UserLeftMessage,
|
type UserLeftMessage,
|
||||||
getOrCreateUserName,
|
getOrCreateUserName,
|
||||||
getColorForUser,
|
getColorForUser,
|
||||||
@@ -93,6 +94,7 @@ export interface CollaborationContextValue {
|
|||||||
requestSync: () => void;
|
requestSync: () => void;
|
||||||
sendSync: (targetConnectionId: string, dataJson: string) => void;
|
sendSync: (targetConnectionId: string, dataJson: string) => void;
|
||||||
sendDataSaved: () => void;
|
sendDataSaved: () => void;
|
||||||
|
broadcastTemplateSync: (templateJson: string, version: number) => void;
|
||||||
|
|
||||||
// Event subscriptions for component-specific handlers
|
// Event subscriptions for component-specific handlers
|
||||||
onDataChanged: (callback: (msg: DataChangeMessage) => void) => () => void;
|
onDataChanged: (callback: (msg: DataChangeMessage) => void) => () => void;
|
||||||
@@ -104,6 +106,7 @@ export interface CollaborationContextValue {
|
|||||||
onSyncRequested: (callback: (msg: SyncRequestMessage) => void) => () => void;
|
onSyncRequested: (callback: (msg: SyncRequestMessage) => void) => () => void;
|
||||||
onSyncReceived: (callback: (msg: SyncDataMessage) => void) => () => void;
|
onSyncReceived: (callback: (msg: SyncDataMessage) => void) => () => void;
|
||||||
onDataSaved: (callback: (msg: DataSavedMessage) => void) => () => void;
|
onDataSaved: (callback: (msg: DataSavedMessage) => void) => () => void;
|
||||||
|
onTemplateSync: (callback: (msg: TemplateSyncMessage) => void) => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CollaborationContext = createContext<CollaborationContextValue | null>(
|
const CollaborationContext = createContext<CollaborationContextValue | null>(
|
||||||
@@ -409,6 +412,13 @@ export function CollaborationProvider({
|
|||||||
collaborationService.sendDataSaved();
|
collaborationService.sendDataSaved();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const broadcastTemplateSync = useCallback(
|
||||||
|
(templateJson: string, version: number) => {
|
||||||
|
collaborationService.broadcastTemplateSync(templateJson, version);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
// Event subscription pass-through
|
// Event subscription pass-through
|
||||||
const onDataChanged = useCallback(
|
const onDataChanged = useCallback(
|
||||||
(callback: (msg: DataChangeMessage) => void) => {
|
(callback: (msg: DataChangeMessage) => void) => {
|
||||||
@@ -459,6 +469,13 @@ export function CollaborationProvider({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onTemplateSync = useCallback(
|
||||||
|
(callback: (msg: TemplateSyncMessage) => void) => {
|
||||||
|
return collaborationService.onTemplateSync(callback);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const value: CollaborationContextValue = {
|
const value: CollaborationContextValue = {
|
||||||
// Connection state
|
// Connection state
|
||||||
isConnected,
|
isConnected,
|
||||||
@@ -499,6 +516,7 @@ export function CollaborationProvider({
|
|||||||
requestSync,
|
requestSync,
|
||||||
sendSync,
|
sendSync,
|
||||||
sendDataSaved,
|
sendDataSaved,
|
||||||
|
broadcastTemplateSync,
|
||||||
|
|
||||||
// Event subscriptions
|
// Event subscriptions
|
||||||
onDataChanged,
|
onDataChanged,
|
||||||
@@ -508,6 +526,7 @@ export function CollaborationProvider({
|
|||||||
onSyncRequested,
|
onSyncRequested,
|
||||||
onSyncReceived,
|
onSyncReceived,
|
||||||
onDataSaved,
|
onDataSaved,
|
||||||
|
onTemplateSync,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -178,6 +178,9 @@ export default function ReportEditorPage() {
|
|||||||
// Auto-save feature - enabled by default
|
// Auto-save feature - enabled by default
|
||||||
const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
|
const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
|
||||||
|
|
||||||
|
// Template version for collaboration sync (increments on each save)
|
||||||
|
const templateVersionRef = useRef(0);
|
||||||
|
|
||||||
// ============ COLLABORATION (using global context) ============
|
// ============ COLLABORATION (using global context) ============
|
||||||
// Room key format: "report-template:{id}"
|
// Room key format: "report-template:{id}"
|
||||||
const roomKey = id ? `report-template:${id}` : null;
|
const roomKey = id ? `report-template:${id}` : null;
|
||||||
@@ -315,19 +318,68 @@ export default function ReportEditorPage() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Template saved by remote user
|
// Template sync received - apply template directly without server reload
|
||||||
|
unsubscribers.push(
|
||||||
|
collaboration.onTemplateSync((message) => {
|
||||||
|
console.log(
|
||||||
|
`[Collaboration] Received TemplateSync, version ${message.version}, size: ${message.templateJson.length} bytes`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only apply if the received version is newer
|
||||||
|
if (message.version <= templateVersionRef.current) {
|
||||||
|
console.log(
|
||||||
|
`[Collaboration] Ignoring older template version ${message.version} (current: ${templateVersionRef.current})`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isApplyingRemoteChange.current = true;
|
||||||
|
const receivedTemplate = JSON.parse(
|
||||||
|
message.templateJson,
|
||||||
|
) as AprtTemplate;
|
||||||
|
|
||||||
|
// Update version to received version
|
||||||
|
templateVersionRef.current = message.version;
|
||||||
|
|
||||||
|
// Apply template without adding to history (it's a sync, not a user action)
|
||||||
|
historyActions.setWithoutHistory(() => receivedTemplate);
|
||||||
|
|
||||||
|
// Show notification
|
||||||
|
setSnackbar({
|
||||||
|
open: true,
|
||||||
|
message: "Template aggiornato da un altro utente",
|
||||||
|
severity: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also update the query cache so it stays in sync
|
||||||
|
queryClient.setQueryData(["report-template", id], (old: unknown) => {
|
||||||
|
if (!old) return old;
|
||||||
|
return {
|
||||||
|
...(old as Record<string, unknown>),
|
||||||
|
templateJson: message.templateJson,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Collaboration] Error parsing received template:", e);
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
isApplyingRemoteChange.current = false;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Legacy DataSaved handler (for backwards compatibility - will be removed)
|
||||||
unsubscribers.push(
|
unsubscribers.push(
|
||||||
collaboration.onDataSaved((message) => {
|
collaboration.onDataSaved((message) => {
|
||||||
console.log(
|
console.log(
|
||||||
"[Collaboration] Received DataSaved from:",
|
"[Collaboration] Received DataSaved from:",
|
||||||
message.savedBy,
|
message.savedBy,
|
||||||
|
"(legacy - should use TemplateSync instead)",
|
||||||
);
|
);
|
||||||
setSnackbar({
|
// Only reload from server if we haven't received a TemplateSync
|
||||||
open: true,
|
// This is a fallback for older clients
|
||||||
message: `${message.savedBy} ha salvato il template`,
|
|
||||||
severity: "success",
|
|
||||||
});
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["report-template", id] });
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -529,16 +581,30 @@ export default function ReportEditorPage() {
|
|||||||
// Mark current state as saved
|
// Mark current state as saved
|
||||||
setLastSavedUndoCount(templateHistory.undoCount);
|
setLastSavedUndoCount(templateHistory.undoCount);
|
||||||
|
|
||||||
// Notify collaborators of save
|
// Broadcast template to collaborators (instant sync without server reload)
|
||||||
console.log(
|
|
||||||
"[AutoSave] Save success, collaboration.isConnected:",
|
|
||||||
collaboration.isConnected,
|
|
||||||
"currentRoom:",
|
|
||||||
collaboration.currentRoom,
|
|
||||||
);
|
|
||||||
if (collaboration.isConnected && collaboration.currentRoom) {
|
if (collaboration.isConnected && collaboration.currentRoom) {
|
||||||
console.log("[AutoSave] Sending DataSaved notification");
|
// Update dataSources in template for broadcasting
|
||||||
collaboration.sendDataSaved();
|
const updatedTemplateForSync = {
|
||||||
|
...template,
|
||||||
|
dataSources: selectedDatasets.reduce(
|
||||||
|
(acc, ds) => {
|
||||||
|
acc[ds.id] = { type: "object" as const, schema: ds.id };
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, { type: "object"; schema: string }>,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Increment version and broadcast
|
||||||
|
templateVersionRef.current += 1;
|
||||||
|
const templateJson = JSON.stringify(updatedTemplateForSync);
|
||||||
|
console.log(
|
||||||
|
`[AutoSave] Broadcasting template sync, version ${templateVersionRef.current}, size: ${templateJson.length} bytes`,
|
||||||
|
);
|
||||||
|
collaboration.broadcastTemplateSync(
|
||||||
|
templateJson,
|
||||||
|
templateVersionRef.current,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
@@ -1452,38 +1518,49 @@ export default function ReportEditorPage() {
|
|||||||
selectedElementId,
|
selectedElementId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Auto-save effect - saves after 1 second of inactivity when there are unsaved changes
|
// Auto-save: simple debounced save on every template change
|
||||||
// Use refs to avoid the effect re-running on every render due to saveMutation changing
|
const autoSaveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const saveMutationRef = useRef(saveMutation);
|
const saveMutationRef = useRef(saveMutation);
|
||||||
saveMutationRef.current = saveMutation;
|
saveMutationRef.current = saveMutation;
|
||||||
|
const templateForSaveRef = useRef(template);
|
||||||
const templateRef = useRef(template);
|
templateForSaveRef.current = template;
|
||||||
templateRef.current = template;
|
const templateInfoForSaveRef = useRef(templateInfo);
|
||||||
|
templateInfoForSaveRef.current = templateInfo;
|
||||||
const templateInfoRef = useRef(templateInfo);
|
|
||||||
templateInfoRef.current = templateInfo;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
// Skip if disabled or new template
|
||||||
!autoSaveEnabled ||
|
if (!autoSaveEnabled || isNew) {
|
||||||
!hasUnsavedChanges ||
|
|
||||||
isNew ||
|
|
||||||
saveMutationRef.current.isPending
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
// Skip if no unsaved changes
|
||||||
|
if (!hasUnsavedChanges) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear previous timeout (debounce)
|
||||||
|
if (autoSaveTimeoutRef.current) {
|
||||||
|
clearTimeout(autoSaveTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save after 500ms debounce
|
||||||
|
autoSaveTimeoutRef.current = setTimeout(() => {
|
||||||
|
// Check if not already saving at the moment of execution
|
||||||
if (!saveMutationRef.current.isPending) {
|
if (!saveMutationRef.current.isPending) {
|
||||||
|
console.log("[AutoSave] Saving...");
|
||||||
saveMutationRef.current.mutate({
|
saveMutationRef.current.mutate({
|
||||||
template: templateRef.current,
|
template: templateForSaveRef.current,
|
||||||
info: templateInfoRef.current,
|
info: templateInfoForSaveRef.current,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 1000); // 1 second debounce
|
}, 500);
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId);
|
return () => {
|
||||||
}, [autoSaveEnabled, hasUnsavedChanges, isNew]);
|
if (autoSaveTimeoutRef.current) {
|
||||||
|
clearTimeout(autoSaveTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [autoSaveEnabled, isNew, hasUnsavedChanges, templateHistory.undoCount]);
|
||||||
|
|
||||||
if (isLoadingTemplate && id) {
|
if (isLoadingTemplate && id) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
|
// Usa variabile ambiente o fallback a window.location per accesso esterno
|
||||||
|
const getApiBaseUrl = (): string => {
|
||||||
|
// Prima controlla la variabile ambiente (deve essere non-vuota)
|
||||||
|
const envUrl = import.meta.env.VITE_API_URL;
|
||||||
|
if (envUrl && envUrl.trim() !== "") {
|
||||||
|
return `${envUrl}/api`;
|
||||||
|
}
|
||||||
|
// Fallback: usa lo stesso host del frontend ma porta 5000
|
||||||
|
const protocol = window.location.protocol;
|
||||||
|
const hostname = window.location.hostname;
|
||||||
|
return `${protocol}//${hostname}:5000/api`;
|
||||||
|
};
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: "http://localhost:5000/api",
|
baseURL: getApiBaseUrl(),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
import * as signalR from "@microsoft/signalr";
|
import * as signalR from "@microsoft/signalr";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
const API_URL = "http://localhost:5000";
|
// Usa variabile ambiente o fallback a window.location per accesso esterno
|
||||||
|
const getApiUrl = (): string => {
|
||||||
|
// Prima controlla la variabile ambiente (deve essere non-vuota)
|
||||||
|
const envUrl = import.meta.env.VITE_API_URL;
|
||||||
|
if (envUrl && envUrl.trim() !== "") {
|
||||||
|
return envUrl;
|
||||||
|
}
|
||||||
|
// Fallback: usa lo stesso host del frontend ma porta 5000
|
||||||
|
const protocol = window.location.protocol;
|
||||||
|
const hostname = window.location.hostname;
|
||||||
|
return `${protocol}//${hostname}:5000`;
|
||||||
|
};
|
||||||
|
|
||||||
const CURSOR_THROTTLE_MS = 50;
|
const CURSOR_THROTTLE_MS = 50;
|
||||||
|
|
||||||
// ==================== TYPES ====================
|
// ==================== TYPES ====================
|
||||||
@@ -108,6 +120,113 @@ export interface DataSavedMessage {
|
|||||||
timestamp: string;
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TemplateSyncMessage {
|
||||||
|
templateJson: string;
|
||||||
|
senderConnectionId: string;
|
||||||
|
version: number;
|
||||||
|
timestamp: string;
|
||||||
|
compressed?: boolean; // Whether the templateJson is base64-encoded gzip
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== COMPRESSION UTILITIES ====================
|
||||||
|
|
||||||
|
const COMPRESSION_THRESHOLD = 10000; // Compress if JSON is > 10KB
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compress a string using gzip and return base64-encoded result
|
||||||
|
*/
|
||||||
|
async function compressString(input: string): Promise<string> {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(input);
|
||||||
|
|
||||||
|
const cs = new CompressionStream("gzip");
|
||||||
|
const writer = cs.writable.getWriter();
|
||||||
|
writer.write(data);
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
const compressedChunks: Uint8Array[] = [];
|
||||||
|
const reader = cs.readable.getReader();
|
||||||
|
|
||||||
|
let result = await reader.read();
|
||||||
|
while (!result.done) {
|
||||||
|
compressedChunks.push(result.value);
|
||||||
|
result = await reader.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine chunks
|
||||||
|
const totalLength = compressedChunks.reduce(
|
||||||
|
(acc, arr) => acc + arr.length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const compressed = new Uint8Array(totalLength);
|
||||||
|
let offset = 0;
|
||||||
|
for (const chunk of compressedChunks) {
|
||||||
|
compressed.set(chunk, offset);
|
||||||
|
offset += chunk.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to base64
|
||||||
|
return btoa(String.fromCharCode(...compressed));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decompress a base64-encoded gzip string
|
||||||
|
*/
|
||||||
|
async function decompressString(base64Input: string): Promise<string> {
|
||||||
|
// Convert base64 to Uint8Array
|
||||||
|
const binaryString = atob(base64Input);
|
||||||
|
const compressed = new Uint8Array(binaryString.length);
|
||||||
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
|
compressed[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ds = new DecompressionStream("gzip");
|
||||||
|
const writer = ds.writable.getWriter();
|
||||||
|
writer.write(compressed);
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
const decompressedChunks: Uint8Array[] = [];
|
||||||
|
const reader = ds.readable.getReader();
|
||||||
|
|
||||||
|
let result = await reader.read();
|
||||||
|
while (!result.done) {
|
||||||
|
decompressedChunks.push(result.value);
|
||||||
|
result = await reader.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine chunks
|
||||||
|
const totalLength = decompressedChunks.reduce(
|
||||||
|
(acc, arr) => acc + arr.length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const decompressed = new Uint8Array(totalLength);
|
||||||
|
let offset = 0;
|
||||||
|
for (const chunk of decompressedChunks) {
|
||||||
|
decompressed.set(chunk, offset);
|
||||||
|
offset += chunk.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
return decoder.decode(decompressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if CompressionStream is available (modern browsers)
|
||||||
|
*/
|
||||||
|
function isCompressionAvailable(): boolean {
|
||||||
|
return (
|
||||||
|
typeof CompressionStream !== "undefined" &&
|
||||||
|
typeof DecompressionStream !== "undefined"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
compressString,
|
||||||
|
decompressString,
|
||||||
|
isCompressionAvailable,
|
||||||
|
COMPRESSION_THRESHOLD,
|
||||||
|
};
|
||||||
|
|
||||||
export interface UserLeftMessage {
|
export interface UserLeftMessage {
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
userName: string;
|
userName: string;
|
||||||
@@ -220,6 +339,7 @@ class CollaborationService {
|
|||||||
private syncRequestedListeners = new Set<EventCallback<SyncRequestMessage>>();
|
private syncRequestedListeners = new Set<EventCallback<SyncRequestMessage>>();
|
||||||
private syncReceivedListeners = new Set<EventCallback<SyncDataMessage>>();
|
private syncReceivedListeners = new Set<EventCallback<SyncDataMessage>>();
|
||||||
private dataSavedListeners = new Set<EventCallback<DataSavedMessage>>();
|
private dataSavedListeners = new Set<EventCallback<DataSavedMessage>>();
|
||||||
|
private templateSyncListeners = new Set<EventCallback<TemplateSyncMessage>>();
|
||||||
private connectionStateListeners = new Set<
|
private connectionStateListeners = new Set<
|
||||||
EventCallback<{ connected: boolean; connecting: boolean }>
|
EventCallback<{ connected: boolean; connecting: boolean }>
|
||||||
>();
|
>();
|
||||||
@@ -295,8 +415,10 @@ class CollaborationService {
|
|||||||
this.notifyConnectionState();
|
this.notifyConnectionState();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const apiUrl = getApiUrl(); // Calcola URL al momento della connessione
|
||||||
|
console.log(`[Collaboration] Connecting to ${apiUrl}/hubs/collaboration`);
|
||||||
this.connection = new signalR.HubConnectionBuilder()
|
this.connection = new signalR.HubConnectionBuilder()
|
||||||
.withUrl(`${API_URL}/hubs/collaboration`)
|
.withUrl(`${apiUrl}/hubs/collaboration`)
|
||||||
.withAutomaticReconnect([0, 1000, 2000, 5000, 10000, 30000])
|
.withAutomaticReconnect([0, 1000, 2000, 5000, 10000, 30000])
|
||||||
.configureLogging(signalR.LogLevel.Warning)
|
.configureLogging(signalR.LogLevel.Warning)
|
||||||
.build();
|
.build();
|
||||||
@@ -639,6 +761,63 @@ class CollaborationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast the full template to all collaborators in the room
|
||||||
|
* This is more efficient than having each client reload from the server
|
||||||
|
* Uses compression for large templates (>10KB)
|
||||||
|
* @param templateJson The full template JSON string
|
||||||
|
* @param version Incrementing version number for conflict detection
|
||||||
|
*/
|
||||||
|
async broadcastTemplateSync(
|
||||||
|
templateJson: string,
|
||||||
|
version: number,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!this.canSend()) {
|
||||||
|
console.log("[Collaboration] broadcastTemplateSync: canSend is false");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataToSend = templateJson;
|
||||||
|
let compressed = false;
|
||||||
|
|
||||||
|
// Compress if large enough and compression is available
|
||||||
|
if (
|
||||||
|
templateJson.length > COMPRESSION_THRESHOLD &&
|
||||||
|
isCompressionAvailable()
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const compressedData = await compressString(templateJson);
|
||||||
|
// Only use compression if it actually reduces size
|
||||||
|
if (compressedData.length < templateJson.length * 0.9) {
|
||||||
|
dataToSend = compressedData;
|
||||||
|
compressed = true;
|
||||||
|
console.log(
|
||||||
|
`[Collaboration] Compressed template: ${templateJson.length} -> ${compressedData.length} bytes (${Math.round((1 - compressedData.length / templateJson.length) * 100)}% reduction)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
"[Collaboration] Compression failed, sending uncompressed:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[Collaboration] Broadcasting template sync, version ${version}, size: ${dataToSend.length} bytes, compressed: ${compressed}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.connection!.invoke(
|
||||||
|
"BroadcastTemplateSync",
|
||||||
|
this.currentRoom,
|
||||||
|
dataToSend,
|
||||||
|
version,
|
||||||
|
compressed,
|
||||||
|
).catch((err) =>
|
||||||
|
console.error("[Collaboration] Error broadcasting template sync:", err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== SUBSCRIPTIONS ====================
|
// ==================== SUBSCRIPTIONS ====================
|
||||||
|
|
||||||
onUserJoined(callback: EventCallback<Collaborator>): () => void {
|
onUserJoined(callback: EventCallback<Collaborator>): () => void {
|
||||||
@@ -713,6 +892,11 @@ class CollaborationService {
|
|||||||
return () => this.dataSavedListeners.delete(callback);
|
return () => this.dataSavedListeners.delete(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTemplateSync(callback: EventCallback<TemplateSyncMessage>): () => void {
|
||||||
|
this.templateSyncListeners.add(callback);
|
||||||
|
return () => this.templateSyncListeners.delete(callback);
|
||||||
|
}
|
||||||
|
|
||||||
onConnectionStateChanged(
|
onConnectionStateChanged(
|
||||||
callback: EventCallback<{ connected: boolean; connecting: boolean }>,
|
callback: EventCallback<{ connected: boolean; connecting: boolean }>,
|
||||||
): () => void {
|
): () => void {
|
||||||
@@ -1006,6 +1190,40 @@ class CollaborationService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Template sync (full template broadcast)
|
||||||
|
this.connection.on("TemplateSync", async (message: TemplateSyncMessage) => {
|
||||||
|
console.log(
|
||||||
|
`[Collaboration] Received TemplateSync, version ${message.version}, size: ${message.templateJson.length} bytes, compressed: ${message.compressed}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Decompress if needed
|
||||||
|
let finalMessage = message;
|
||||||
|
if (message.compressed) {
|
||||||
|
try {
|
||||||
|
const decompressedJson = await decompressString(message.templateJson);
|
||||||
|
console.log(
|
||||||
|
`[Collaboration] Decompressed template: ${message.templateJson.length} -> ${decompressedJson.length} bytes`,
|
||||||
|
);
|
||||||
|
finalMessage = {
|
||||||
|
...message,
|
||||||
|
templateJson: decompressedJson,
|
||||||
|
compressed: false,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Collaboration] Decompression failed:", e);
|
||||||
|
return; // Don't notify listeners with corrupted data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.templateSyncListeners.forEach((cb) => {
|
||||||
|
try {
|
||||||
|
cb(finalMessage);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Collaboration] Listener error:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Reconnection handling
|
// Reconnection handling
|
||||||
this.connection.onreconnecting(() => {
|
this.connection.onreconnecting(() => {
|
||||||
console.log("[Collaboration] Reconnecting...");
|
console.log("[Collaboration] Reconnecting...");
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import * as signalR from "@microsoft/signalr";
|
import * as signalR from "@microsoft/signalr";
|
||||||
|
|
||||||
const API_URL = "http://localhost:5000";
|
// Usa variabile ambiente o fallback a window.location per accesso esterno
|
||||||
|
const getApiUrl = (): string => {
|
||||||
|
// Prima controlla la variabile ambiente (deve essere non-vuota)
|
||||||
|
const envUrl = import.meta.env.VITE_API_URL;
|
||||||
|
if (envUrl && envUrl.trim() !== "") {
|
||||||
|
return envUrl;
|
||||||
|
}
|
||||||
|
// Fallback: usa lo stesso host del frontend ma porta 5000
|
||||||
|
const protocol = window.location.protocol;
|
||||||
|
const hostname = window.location.hostname;
|
||||||
|
return `${protocol}//${hostname}:5000`;
|
||||||
|
};
|
||||||
|
|
||||||
class SignalRService {
|
class SignalRService {
|
||||||
private connection: signalR.HubConnection | null = null;
|
private connection: signalR.HubConnection | null = null;
|
||||||
@@ -24,8 +35,10 @@ class SignalRService {
|
|||||||
this.isConnecting = true;
|
this.isConnecting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const apiUrl = getApiUrl(); // Calcola URL al momento della connessione
|
||||||
|
console.log(`[SignalR Data] Connecting to ${apiUrl}/hubs/data`);
|
||||||
this.connection = new signalR.HubConnectionBuilder()
|
this.connection = new signalR.HubConnectionBuilder()
|
||||||
.withUrl(`${API_URL}/hubs/data`)
|
.withUrl(`${apiUrl}/hubs/data`)
|
||||||
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
|
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
|
||||||
.configureLogging(signalR.LogLevel.Information)
|
.configureLogging(signalR.LogLevel.Information)
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -309,6 +309,30 @@ public class CollaborationHub : Hub
|
|||||||
Console.WriteLine($"[Collaboration] DataSaved sent to others in room {roomKey}");
|
Console.WriteLine($"[Collaboration] DataSaved sent to others in room {roomKey}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sincronizza l'intero template con tutti gli altri collaboratori nella room
|
||||||
|
/// Usato per sync efficienti - invia il template completo invece di richiedere reload dal server
|
||||||
|
/// Supporta compressione opzionale per template grandi
|
||||||
|
/// </summary>
|
||||||
|
public async Task BroadcastTemplateSync(string roomKey, string templateJson, int version, bool compressed = false)
|
||||||
|
{
|
||||||
|
var senderId = Context.ConnectionId;
|
||||||
|
Console.WriteLine($"[Collaboration] BroadcastTemplateSync from {senderId} for room {roomKey}, version {version}, size: {templateJson.Length} bytes, compressed: {compressed}");
|
||||||
|
|
||||||
|
UpdateUserActivity(senderId);
|
||||||
|
|
||||||
|
await Clients.OthersInGroup(roomKey).SendAsync("TemplateSync", new TemplateSyncMessage
|
||||||
|
{
|
||||||
|
TemplateJson = templateJson,
|
||||||
|
SenderConnectionId = senderId,
|
||||||
|
Version = version,
|
||||||
|
Compressed = compressed,
|
||||||
|
Timestamp = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
|
||||||
|
Console.WriteLine($"[Collaboration] TemplateSync broadcast complete for room {roomKey}");
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Lifecycle
|
#region Lifecycle
|
||||||
@@ -517,6 +541,19 @@ public class DataSavedMessage
|
|||||||
public DateTime Timestamp { get; set; }
|
public DateTime Timestamp { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message for efficient template sync - sends full template JSON (compressed optional)
|
||||||
|
/// Used for initial sync and full resync requests
|
||||||
|
/// </summary>
|
||||||
|
public class TemplateSyncMessage
|
||||||
|
{
|
||||||
|
public string TemplateJson { get; set; } = string.Empty;
|
||||||
|
public string SenderConnectionId { get; set; } = string.Empty;
|
||||||
|
public int Version { get; set; } // Incrementing version number for conflict detection
|
||||||
|
public bool Compressed { get; set; } // Whether TemplateJson is base64-encoded gzip
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class UserLeftMessage
|
public class UserLeftMessage
|
||||||
{
|
{
|
||||||
public string ConnectionId { get; set; } = string.Empty;
|
public string ConnectionId { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -12,19 +12,50 @@ public class DataHub : Hub
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task NotifyDataChanged(string entityType, string action, object? data = null)
|
public async Task NotifyDataChanged(string entityType, string action, object? data = null)
|
||||||
{
|
{
|
||||||
await Clients.Others.SendAsync("DataChanged", entityType, action, data);
|
try
|
||||||
|
{
|
||||||
|
await Clients.Others.SendAsync("DataChanged", entityType, action, data);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[DataHub] Error in NotifyDataChanged: {ex.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnConnectedAsync()
|
public override async Task OnConnectedAsync()
|
||||||
{
|
{
|
||||||
await base.OnConnectedAsync();
|
try
|
||||||
Console.WriteLine($"Client connected: {Context.ConnectionId}");
|
{
|
||||||
|
Console.WriteLine($"[DataHub] Client connecting: {Context.ConnectionId}");
|
||||||
|
await base.OnConnectedAsync();
|
||||||
|
Console.WriteLine($"[DataHub] Client connected: {Context.ConnectionId}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[DataHub] Error in OnConnectedAsync: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||||
{
|
{
|
||||||
await base.OnDisconnectedAsync(exception);
|
try
|
||||||
Console.WriteLine($"Client disconnected: {Context.ConnectionId}");
|
{
|
||||||
|
if (exception != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[DataHub] Client disconnected with error: {Context.ConnectionId} - {exception.Message}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[DataHub] Client disconnected: {Context.ConnectionId}");
|
||||||
|
}
|
||||||
|
await base.OnDisconnectedAsync(exception);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[DataHub] Error in OnDisconnectedAsync: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,20 +19,23 @@ builder.Services.AddScoped<DemoDataService>();
|
|||||||
builder.Services.AddScoped<ReportGeneratorService>();
|
builder.Services.AddScoped<ReportGeneratorService>();
|
||||||
builder.Services.AddSingleton<DataNotificationService>();
|
builder.Services.AddSingleton<DataNotificationService>();
|
||||||
|
|
||||||
// SignalR
|
// SignalR - with increased message size for template sync (default is 32KB)
|
||||||
builder.Services.AddSignalR()
|
builder.Services.AddSignalR(options =>
|
||||||
|
{
|
||||||
|
options.MaximumReceiveMessageSize = 1024 * 1024; // 1MB max message size
|
||||||
|
})
|
||||||
.AddJsonProtocol(options =>
|
.AddJsonProtocol(options =>
|
||||||
{
|
{
|
||||||
options.PayloadSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
|
options.PayloadSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
|
||||||
options.PayloadSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
options.PayloadSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||||
});
|
});
|
||||||
|
|
||||||
// CORS - Allow credentials for SignalR
|
// CORS - Allow credentials for SignalR (accepts any origin for external access)
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
options.AddPolicy("AllowFrontend", policy =>
|
options.AddPolicy("AllowFrontend", policy =>
|
||||||
{
|
{
|
||||||
policy.SetIsOriginAllowed(origin => new Uri(origin).Host == "localhost")
|
policy.SetIsOriginAllowed(_ => true) // Permette qualsiasi origine
|
||||||
.AllowAnyHeader()
|
.AllowAnyHeader()
|
||||||
.AllowAnyMethod()
|
.AllowAnyMethod()
|
||||||
.AllowCredentials();
|
.AllowCredentials();
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user