From c1633b72d1105fa421a48ac49b4f7fcbb0e451b4 Mon Sep 17 00:00:00 2001 From: Matteo Giustini Date: Thu, 18 Jun 2026 12:43:03 +0200 Subject: [PATCH] fix frontend --- backend/app/api/v1/messages.py | 22 +++++-- backend/app/services/mailbox_service.py | 8 +++ frontend/src/pages/Inbox/InboxPage.tsx | 66 ++++++++++++------- .../pages/MessageDetail/MessageDetailPage.tsx | 20 ++++++ worker/app/main.py | 3 +- worker/app/models.py | 5 ++ worker/pyproject.toml | 3 + 7 files changed, 96 insertions(+), 31 deletions(-) diff --git a/backend/app/api/v1/messages.py b/backend/app/api/v1/messages.py index 5386612..afe97a0 100644 --- a/backend/app/api/v1/messages.py +++ b/backend/app/api/v1/messages.py @@ -33,7 +33,7 @@ from app.services.audit_service import get_real_ip from app.services.search_service import SearchService from app.config import get_settings -from app.core.exceptions import ForbiddenError, NotFoundError +from app.core.exceptions import ConflictError, ForbiddenError, NotFoundError from app.database import get_db from app.dependencies import CurrentUser, DB from app.models.label import Label @@ -446,11 +446,15 @@ async def bulk_update_messages( elif not data.is_trashed: message.trashed_at = None if data.is_pending_conservation is not None: - message.is_pending_conservation = data.is_pending_conservation - if data.is_pending_conservation and not message.pending_conservation_at: - message.pending_conservation_at = now - elif not data.is_pending_conservation: - message.pending_conservation_at = None + # Guard bulk: non rimandare in conservazione messaggi gia' conservati + if data.is_pending_conservation and message.is_conserved: + pass # skip — gia' conservato, non viene ri-accodato + else: + message.is_pending_conservation = data.is_pending_conservation + if data.is_pending_conservation and not message.pending_conservation_at: + message.pending_conservation_at = now + elif not data.is_pending_conservation: + message.pending_conservation_at = None if data.is_conserved is not None: message.is_conserved = data.is_conserved if data.is_conserved and not message.conserved_at: @@ -551,6 +555,12 @@ async def update_message( perm_svc = PermissionService(db) await perm_svc.require_can_conserve(current_user, message.mailbox_id) + # Guard: blocca re-accodamento di messaggi gia' conservati + if data.is_pending_conservation is True and message.is_conserved: + raise ConflictError( + "Questo messaggio e' gia' stato conservato e non puo' essere rimandato in conservazione." + ) + now = datetime.now(timezone.utc) ip = get_real_ip(request) ua = request.headers.get("user-agent") diff --git a/backend/app/services/mailbox_service.py b/backend/app/services/mailbox_service.py index 94d2601..ad1e9b9 100644 --- a/backend/app/services/mailbox_service.py +++ b/backend/app/services/mailbox_service.py @@ -69,6 +69,8 @@ class MailboxService: email_address=str(data.email_address), display_name=data.display_name, provider=data.provider, + protocol_type=data.protocol_type, + rem_provider=data.rem_provider, # Cifra tutte le credenziali IMAP imap_host_enc=encrypt_credential(data.imap_host), imap_port_enc=encrypt_credential(str(data.imap_port)), @@ -149,6 +151,10 @@ class MailboxService: mailbox.provider = data.provider if data.status is not None: mailbox.status = data.status + if data.protocol_type is not None: + mailbox.protocol_type = data.protocol_type + if data.rem_provider is not None: + mailbox.rem_provider = data.rem_provider # IMAP if data.imap_host is not None: @@ -382,6 +388,8 @@ class MailboxService: "last_sync_uid": mailbox.last_sync_uid, "sync_error_msg": mailbox.sync_error_msg, "sync_error_count": mailbox.sync_error_count, + "protocol_type": mailbox.protocol_type, + "rem_provider": mailbox.rem_provider, "created_by": mailbox.created_by, "created_at": mailbox.created_at, "updated_at": mailbox.updated_at, diff --git a/frontend/src/pages/Inbox/InboxPage.tsx b/frontend/src/pages/Inbox/InboxPage.tsx index 182bc7e..e2d4ff4 100644 --- a/frontend/src/pages/Inbox/InboxPage.tsx +++ b/frontend/src/pages/Inbox/InboxPage.tsx @@ -1067,6 +1067,15 @@ function MessageRow({
+ {message.is_conserved && ( + + + Conservato + + )} {formatRelative(message.received_at || message.sent_at || message.created_at)} @@ -1211,31 +1220,40 @@ function MessageRow({ )} - {/* Invia a Conservazione / Rimuovi da Da Conservare */} - {canConserve && viewMode !== 'trash' && viewMode !== 'conservation_archived' && ( - + : hovered + ? 'opacity-100' + : 'opacity-0 pointer-events-none', + )} + > + {viewMode === 'conservation_pending' ? ( + + ) : ( + + )} + + ) )} {/* Indicatore allegati */} diff --git a/frontend/src/pages/MessageDetail/MessageDetailPage.tsx b/frontend/src/pages/MessageDetail/MessageDetailPage.tsx index 9a68263..f56c6be 100644 --- a/frontend/src/pages/MessageDetail/MessageDetailPage.tsx +++ b/frontend/src/pages/MessageDetail/MessageDetailPage.tsx @@ -27,6 +27,7 @@ import { ChevronDown, FolderOpen, Plus, + ShieldCheck, } from 'lucide-react' import toast from 'react-hot-toast' import { Button } from '@/components/ui/Button' @@ -1162,6 +1163,25 @@ export function MessageDetailPage() {
)} + {/* Banner "Conservato" */} + {message.is_conserved && ( +
+ + + Questo messaggio e' stato conservato in modo sostitutivo + {message.conserved_at + ? ` il ${new Date(message.conserved_at).toLocaleDateString('it-IT', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + })}` + : ''}. + +
+ )} + {/* Contenuto */}
diff --git a/worker/app/main.py b/worker/app/main.py index 0455d26..97f0c5d 100644 --- a/worker/app/main.py +++ b/worker/app/main.py @@ -135,7 +135,8 @@ class WorkerSettings: """Configurazione del worker arq.""" # Funzioni/job registrati (code-driven, on-demand) - functions = [sync_mailbox, send_pec, watch_receipt, dispatch_notification, apply_routing_rules, health_check] + # run_conservation e' incluso anche qui per permettere l'avvio manuale tramite enqueue_job + functions = [sync_mailbox, send_pec, watch_receipt, dispatch_notification, apply_routing_rules, health_check, run_conservation] # Job schedulati (cron) # run_conservation: ogni giorno alle 16:00 UTC = 18:00 ora Italia (CEST, UTC+2) diff --git a/worker/app/models.py b/worker/app/models.py index a1c16f5..3207bab 100644 --- a/worker/app/models.py +++ b/worker/app/models.py @@ -125,6 +125,11 @@ class Message(Base): is_archived: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) archived_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + # Conservazione sostitutiva + is_pending_conservation: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + is_conserved: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + conserved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + # Protocollo e REM europea (Feature N8) protocol_type: Mapped[str] = mapped_column(String(10), nullable=False, default="pec_it") rem_evidence_type: Mapped[str | None] = mapped_column(String(100), nullable=True) diff --git a/worker/pyproject.toml b/worker/pyproject.toml index e63197c..d7532b3 100644 --- a/worker/pyproject.toml +++ b/worker/pyproject.toml @@ -38,6 +38,9 @@ dependencies = [ "python-jose[cryptography]>=3.3.0", "bcrypt>=4.0.0", + # HTTP client (conservatore AgID, upload SIP) + "httpx>=0.27.0", + # Utilities "python-dotenv>=1.0.0", "email-validator>=2.2.0",