fix frontend

This commit is contained in:
2026-06-18 12:43:03 +02:00
parent 042a522854
commit c1633b72d1
7 changed files with 96 additions and 31 deletions
+16 -6
View File
@@ -33,7 +33,7 @@ from app.services.audit_service import get_real_ip
from app.services.search_service import SearchService from app.services.search_service import SearchService
from app.config import get_settings 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.database import get_db
from app.dependencies import CurrentUser, DB from app.dependencies import CurrentUser, DB
from app.models.label import Label from app.models.label import Label
@@ -446,11 +446,15 @@ async def bulk_update_messages(
elif not data.is_trashed: elif not data.is_trashed:
message.trashed_at = None message.trashed_at = None
if data.is_pending_conservation is not None: if data.is_pending_conservation is not None:
message.is_pending_conservation = data.is_pending_conservation # Guard bulk: non rimandare in conservazione messaggi gia' conservati
if data.is_pending_conservation and not message.pending_conservation_at: if data.is_pending_conservation and message.is_conserved:
message.pending_conservation_at = now pass # skip — gia' conservato, non viene ri-accodato
elif not data.is_pending_conservation: else:
message.pending_conservation_at = 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
if data.is_conserved is not None: if data.is_conserved is not None:
message.is_conserved = data.is_conserved message.is_conserved = data.is_conserved
if data.is_conserved and not message.conserved_at: if data.is_conserved and not message.conserved_at:
@@ -551,6 +555,12 @@ async def update_message(
perm_svc = PermissionService(db) perm_svc = PermissionService(db)
await perm_svc.require_can_conserve(current_user, message.mailbox_id) 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) now = datetime.now(timezone.utc)
ip = get_real_ip(request) ip = get_real_ip(request)
ua = request.headers.get("user-agent") ua = request.headers.get("user-agent")
+8
View File
@@ -69,6 +69,8 @@ class MailboxService:
email_address=str(data.email_address), email_address=str(data.email_address),
display_name=data.display_name, display_name=data.display_name,
provider=data.provider, provider=data.provider,
protocol_type=data.protocol_type,
rem_provider=data.rem_provider,
# Cifra tutte le credenziali IMAP # Cifra tutte le credenziali IMAP
imap_host_enc=encrypt_credential(data.imap_host), imap_host_enc=encrypt_credential(data.imap_host),
imap_port_enc=encrypt_credential(str(data.imap_port)), imap_port_enc=encrypt_credential(str(data.imap_port)),
@@ -149,6 +151,10 @@ class MailboxService:
mailbox.provider = data.provider mailbox.provider = data.provider
if data.status is not None: if data.status is not None:
mailbox.status = data.status 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 # IMAP
if data.imap_host is not None: if data.imap_host is not None:
@@ -382,6 +388,8 @@ class MailboxService:
"last_sync_uid": mailbox.last_sync_uid, "last_sync_uid": mailbox.last_sync_uid,
"sync_error_msg": mailbox.sync_error_msg, "sync_error_msg": mailbox.sync_error_msg,
"sync_error_count": mailbox.sync_error_count, "sync_error_count": mailbox.sync_error_count,
"protocol_type": mailbox.protocol_type,
"rem_provider": mailbox.rem_provider,
"created_by": mailbox.created_by, "created_by": mailbox.created_by,
"created_at": mailbox.created_at, "created_at": mailbox.created_at,
"updated_at": mailbox.updated_at, "updated_at": mailbox.updated_at,
+42 -24
View File
@@ -1067,6 +1067,15 @@ function MessageRow({
</div> </div>
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
{message.is_conserved && (
<span
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-teal-100 text-teal-700 border border-teal-200 flex-shrink-0"
title={`Conservato${message.conserved_at ? ' il ' + new Date(message.conserved_at).toLocaleDateString('it-IT') : ''}`}
>
<ShieldCheck className="h-3 w-3" />
Conservato
</span>
)}
<PecStateBadge state={message.state} /> <PecStateBadge state={message.state} />
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{formatRelative(message.received_at || message.sent_at || message.created_at)} {formatRelative(message.received_at || message.sent_at || message.created_at)}
@@ -1211,31 +1220,40 @@ function MessageRow({
</button> </button>
)} )}
{/* Invia a Conservazione / Rimuovi da Da Conservare */} {/* Conservazione: badge fisso se gia' conservato, pulsante interattivo altrimenti */}
{canConserve && viewMode !== 'trash' && viewMode !== 'conservation_archived' && ( {canConserve && viewMode !== 'trash' && (
<button message.is_conserved ? (
onClick={onToggleConserve} <span
title={viewMode === 'conservation_pending' ? 'Rimuovi da Da Conservare' : 'Invia a Conservazione'} title={`Gia' conservato${message.conserved_at ? ' il ' + new Date(message.conserved_at).toLocaleDateString('it-IT') : ''}`}
className={cn( className="p-1 cursor-default"
'p-1 rounded hover:bg-muted transition-all', >
message.is_pending_conservation <ShieldCheck className="h-4 w-4 text-teal-500" />
? 'opacity-100' </span>
: hovered ) : (
<button
onClick={onToggleConserve}
title={viewMode === 'conservation_pending' ? 'Rimuovi da Da Conservare' : 'Invia a Conservazione'}
className={cn(
'p-1 rounded hover:bg-muted transition-all',
message.is_pending_conservation
? 'opacity-100' ? 'opacity-100'
: 'opacity-0 pointer-events-none', : hovered
)} ? 'opacity-100'
> : 'opacity-0 pointer-events-none',
{viewMode === 'conservation_pending' ? ( )}
<ShieldX className="h-4 w-4 text-orange-500" /> >
) : ( {viewMode === 'conservation_pending' ? (
<ShieldCheck <ShieldX className="h-4 w-4 text-orange-500" />
className={cn( ) : (
'h-4 w-4', <ShieldCheck
message.is_pending_conservation ? 'text-teal-600' : 'text-muted-foreground', className={cn(
)} 'h-4 w-4',
/> message.is_pending_conservation ? 'text-teal-600' : 'text-muted-foreground',
)} )}
</button> />
)}
</button>
)
)} )}
{/* Indicatore allegati */} {/* Indicatore allegati */}
@@ -27,6 +27,7 @@ import {
ChevronDown, ChevronDown,
FolderOpen, FolderOpen,
Plus, Plus,
ShieldCheck,
} from 'lucide-react' } from 'lucide-react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
@@ -1162,6 +1163,25 @@ export function MessageDetailPage() {
</div> </div>
)} )}
{/* Banner "Conservato" */}
{message.is_conserved && (
<div className="bg-teal-50 border-b border-teal-200 px-6 py-2.5 flex items-center gap-3">
<ShieldCheck className="h-4 w-4 text-teal-600 flex-shrink-0" />
<span className="text-sm text-teal-700">
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',
})}`
: ''}.
</span>
</div>
)}
{/* Contenuto */} {/* Contenuto */}
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<div className="max-w-4xl mx-auto px-6 py-8 space-y-6"> <div className="max-w-4xl mx-auto px-6 py-8 space-y-6">
+2 -1
View File
@@ -135,7 +135,8 @@ class WorkerSettings:
"""Configurazione del worker arq.""" """Configurazione del worker arq."""
# Funzioni/job registrati (code-driven, on-demand) # 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) # Job schedulati (cron)
# run_conservation: ogni giorno alle 16:00 UTC = 18:00 ora Italia (CEST, UTC+2) # run_conservation: ogni giorno alle 16:00 UTC = 18:00 ora Italia (CEST, UTC+2)
+5
View File
@@ -125,6 +125,11 @@ class Message(Base):
is_archived: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_archived: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
archived_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) 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) # Protocollo e REM europea (Feature N8)
protocol_type: Mapped[str] = mapped_column(String(10), nullable=False, default="pec_it") 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) rem_evidence_type: Mapped[str | None] = mapped_column(String(100), nullable=True)
+3
View File
@@ -38,6 +38,9 @@ dependencies = [
"python-jose[cryptography]>=3.3.0", "python-jose[cryptography]>=3.3.0",
"bcrypt>=4.0.0", "bcrypt>=4.0.0",
# HTTP client (conservatore AgID, upload SIP)
"httpx>=0.27.0",
# Utilities # Utilities
"python-dotenv>=1.0.0", "python-dotenv>=1.0.0",
"email-validator>=2.2.0", "email-validator>=2.2.0",