From 6b11ab7028de7df7480e380b4aa46fbd0a1dd7de Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Sat, 25 Jul 2026 19:43:29 +0200 Subject: [PATCH 1/8] ++ fix permessi assegnazioni --- ...e_settings_manage_from_assistente_role.php | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 database/migrations/2026_07_25_000001_revoke_settings_manage_from_assistente_role.php diff --git a/database/migrations/2026_07_25_000001_revoke_settings_manage_from_assistente_role.php b/database/migrations/2026_07_25_000001_revoke_settings_manage_from_assistente_role.php new file mode 100644 index 0000000..6b68760 --- /dev/null +++ b/database/migrations/2026_07_25_000001_revoke_settings_manage_from_assistente_role.php @@ -0,0 +1,51 @@ +forgetCachedPermissions(); + + $role = Role::where('name', 'assistente')->first(); + $permission = Permission::where('name', 'settings.manage')->first(); + + if ($role && $permission && $role->hasPermissionTo($permission)) { + $role->revokePermissionTo($permission); + } + + app(PermissionRegistrar::class)->forgetCachedPermissions(); + } + + /** + * Intentionally irreversible. Re-granting settings.manage to the + * assistente role would reintroduce the very ability this migration + * removes, so rolling back must not do it. + */ + public function down(): void + { + // no-op + } +}; From 3ec2422fbc2ccebd555560aab68e635d373807e6 Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Sat, 25 Jul 2026 19:53:12 +0200 Subject: [PATCH 2/8] ++ update HANDOFF.md --- HANDOFF.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/HANDOFF.md b/HANDOFF.md index 12cd74c..7f1a59c 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -4,7 +4,7 @@ Stato del progetto per chi ci mette mano dopo. Per installazione, deploy e uso quotidiano vedi [README.md](README.md); qui c'è il contesto che il README non può dare: perché le cose sono come sono, e cosa resta aperto. -Ultimo aggiornamento: **18 luglio 2026**. +Ultimo aggiornamento: **25 luglio 2026**. --- @@ -89,6 +89,29 @@ messaggio d'errore è più chiaro). Deliberato: un errore immediato è meglio di un database in produzione con password `secret`. +### Modifica/cancellazione delle assegnazioni: solo amministratore + +L'unico punto in cui un'assegnazione **esistente** si modifica o si cancella è +il CRUD del Registro (`Livewire/Registro`: `openCreate`/`openEdit`/`save`/ +`askDelete`/`deleteConfirmed`), blindato su `settings.manage` a due livelli — +`abort_if(..., 403)` nel componente e `@can('settings.manage')` in +`registro.blade.php`, che nasconde pulsanti e modali. `settings.manage` ce l'ha +solo l'`amministratore`. + +Di conseguenza gli altri ruoli **non** possono alterare le assegnazioni: la +pagina Assegna solo ne crea (`territori.assign`), la pagina Rientra solo le +chiude (`territori.return`). Nello specifico l'`assistente` (`registro.view` + +`territori.assign` + `territori.return`) vede il Registro, crea nuove +assegnazioni e le fa rientrare, ma non le modifica né le elimina. + +Poiché il seeder concede in modo additivo e non revoca mai (vedi la nota su +`syncPermissions()` più sotto), un permesso `settings.manage` concesso a mano +all'`assistente` in passato sopravviverebbe a ogni re-seed. La migration +`2026_07_25_000001_revoke_settings_manage_from_assistente_role` chiude questo +drift per i database esistenti: revoca `settings.manage` dal ruolo `assistente` +se presente. È idempotente (no-op sul baseline) e volutamente irreversibile — +il `down()` non ripristina il permesso. + ### Un solo container esegue le migrazioni Solo `app` migra. `queue-worker` e `scheduler` usano `queue-entrypoint.sh`, che From 754adf98788b77b26ad24d448fcca0bd9acc5159 Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Mon, 31 Aug 2026 12:28:53 +0200 Subject: [PATCH 3/8] ++ Add S-12 Template --- S-12_I.pdf | Bin 0 -> 15819 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 S-12_I.pdf diff --git a/S-12_I.pdf b/S-12_I.pdf new file mode 100644 index 0000000000000000000000000000000000000000..36bd1b1cb1ef5fec647281411f28668f482f6956 GIT binary patch literal 15819 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 literal 0 HcmV?d00001 From 223d44300c950af209bc3b8a82edf2e24c489dfd Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Mon, 19 Oct 2026 10:18:09 +0200 Subject: [PATCH 4/8] ++ fix statistiche campagne MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dettaglio campagna: mostra tutti i territori conteggiati (stessa base della percentuale), non solo quelli assegnati dopo l'inizio. - Rientro: se più campagne corrispondono propone la più recente invece della prima creata; testo del banner chiarito. - Dettaglio campagna: nuova sezione "In corso" con i territori fuori durante la campagna e non ancora rientrati. - README: regola di conteggio documentata. Co-Authored-By: Claude Opus 5.5 --- README.md | 12 +++++-- app/Livewire/Campagne/CampagnaShow.php | 24 +++++++++---- app/Models/Assegnazione.php | 12 +++++-- app/Models/Campagna.php | 10 ++++++ .../livewire/assegnazioni/rientra.blade.php | 6 ++-- .../livewire/campagne/campagna-show.blade.php | 36 +++++++++++++++++++ 6 files changed, 87 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c8ab211..491e9c2 100644 --- a/README.md +++ b/README.md @@ -684,8 +684,16 @@ sempre** sul flag: l'amministratore può aggiungere priorità, non toglierla quando la soglia è scattata. ### Percentuale di percorrenza delle campagne -- **Denominatore**: tutti i territori assegnati nel periodo della campagna -- **Numeratore**: le assegnazioni contrassegnate come conteggiate +- **Denominatore**: tutti i territori attivi +- **Numeratore**: le assegnazioni collegate alla campagna e contrassegnate come + conteggiate al rientro + +Al rientro la campagna viene proposta se il territorio era fuori almeno in +parte del periodo: assegnato durante la campagna, rientrato durante la +campagna, oppure fuori per tutta la sua durata. Se sia stato davvero percorso +lo decide l'operatore con la spunta. Se più campagne corrispondono viene +proposta la più recente. I territori fuori durante la campagna e non ancora +rientrati compaiono nel dettaglio come "in corso". --- diff --git a/app/Livewire/Campagne/CampagnaShow.php b/app/Livewire/Campagne/CampagnaShow.php index a18febf..8ba2d6e 100644 --- a/app/Livewire/Campagne/CampagnaShow.php +++ b/app/Livewire/Campagne/CampagnaShow.php @@ -18,16 +18,27 @@ class CampagnaShow extends Component public function render() { - // Assignments counted for this campaign: - // - assigned on or after campaign start - // - linked to this campaign (campaign_id), regardless of returned_at (retroactive returns allowed) - $conteggiate = Assegnazione::where('campaign_id', $this->campagna->id) - ->where('counted_in_campaign', true) - ->where('assigned_at', '>=', $this->campagna->start_date) + // Assignments counted for this campaign: linked via campaign_id and + // flagged as counted — the same set used by the percentage + // (Campagna::percentuale_percorrenza). Which campaign is offered on + // return is decided by Assegnazione::campagnaApplicabile(). + $conteggiate = $this->campagna->assegnazioniConteggiate() ->with(['territorio', 'proclamatore']) ->orderBy('returned_at') ->get(); + // Open assignments can still be counted for this campaign only while + // no later campaign has started: on return the most recent one is + // offered (see Assegnazione::campagnaApplicabile()). + $superata = Campagna::where('start_date', '>', $this->campagna->start_date) + ->where('start_date', '<=', now()->toDateString()) + ->exists(); + + $inCorso = $superata ? null : $this->campagna->assegnazioniInCorso() + ->with(['territorio', 'proclamatore']) + ->orderBy('assigned_at') + ->get(); + // All assignments that were active during this campaign range $assegnateNelRange = Assegnazione::where('assigned_at', '<=', $this->campagna->end_date) ->where(function ($q) { @@ -38,6 +49,7 @@ class CampagnaShow extends Component return view('livewire.campagne.campagna-show', [ 'conteggiate' => $conteggiate, + 'inCorso' => $inCorso, 'assegnateNelRange' => $assegnateNelRange, ]); } diff --git a/app/Models/Assegnazione.php b/app/Models/Assegnazione.php index 01e99d6..f3ad3a2 100644 --- a/app/Models/Assegnazione.php +++ b/app/Models/Assegnazione.php @@ -203,10 +203,18 @@ class Assegnazione extends Model */ public function campagnaApplicabile(?\Carbon\Carbon $returnDate = null): ?Campagna { - $returnDate = $returnDate ?? now(); + $returnDate = ($returnDate ?? now())->toDateString(); + $assignedAt = $this->assigned_at->toDateString(); + // Il territorio può essere conteggiato per una campagna se è rimasto + // in mano al proclamatore almeno in parte del periodo: assegnato + // durante la campagna, rientrato durante la campagna, oppure fuori per + // tutta la sua durata (assegnato prima e rientrato dopo). Se sia stato + // davvero percorso lo decide l'operatore con la spunta. + // Se più campagne corrispondono si propone la più recente. return Campagna::where('start_date', '<=', $returnDate) - ->where('end_date', '>=', $this->assigned_at) + ->where('end_date', '>=', $assignedAt) + ->orderByDesc('start_date') ->first(); } } diff --git a/app/Models/Campagna.php b/app/Models/Campagna.php index b5f0678..121c9d3 100644 --- a/app/Models/Campagna.php +++ b/app/Models/Campagna.php @@ -60,6 +60,16 @@ class Campagna extends Model ->where('counted_in_campaign', true); } + /** + * Assignments still open that were out during this campaign (assigned + * on or before its end): they can be counted for it when they come back. + */ + public function assegnazioniInCorso() + { + return Assegnazione::whereNull('returned_at') + ->where('assigned_at', '<=', $this->end_date); + } + /** * All assignments with assigned_at in this campaign's range. */ diff --git a/resources/views/livewire/assegnazioni/rientra.blade.php b/resources/views/livewire/assegnazioni/rientra.blade.php index 70aaaae..79000ca 100644 --- a/resources/views/livewire/assegnazioni/rientra.blade.php +++ b/resources/views/livewire/assegnazioni/rientra.blade.php @@ -40,12 +40,12 @@ @if($showCampaignPrompt && $campagna)

- ⚠️ Campagna Attiva: {{ $campagna->descrizione }} + ⚠️ Campagna: {{ $campagna->descrizione }}

- Questo territorio è stato assegnato durante la campagna "{{ $campagna->descrizione }}" + Questo territorio era assegnato durante la campagna "{{ $campagna->descrizione }}" ({{ $campagna->start_date->format('d/m/Y') }} — {{ $campagna->end_date->format('d/m/Y') }}). - Vuoi conteggiarlo nella percentuale di percorrenza? + È stato percorso per la campagna? Se sì, conteggialo nella percentuale di percorrenza.

diff --git a/resources/views/livewire/campagne/campagna-show.blade.php b/resources/views/livewire/campagne/campagna-show.blade.php index efa5f5e..fa0e779 100644 --- a/resources/views/livewire/campagne/campagna-show.blade.php +++ b/resources/views/livewire/campagne/campagna-show.blade.php @@ -78,4 +78,40 @@
+ + {{-- Out during the campaign, not yet returned (hidden once a later campaign has started) --}} + @if($inCorso !== null) +
+
+

In corso ({{ $inCorso->count() }})

+

Fuori durante la campagna e non ancora rientrati: si potranno conteggiare al rientro.

+
+
+ + + + + + + + + + + @forelse($inCorso as $a) + + + + + + + @empty + + + + @endforelse + +
TerritorioProclamatoreAssegnatoGiorni
N° {{ $a->territorio?->numero }}{{ $a->proclamatore?->nome_completo ?? 'N/A' }}{{ $a->assigned_at->format('d/m/Y') }}{{ $a->giorni }}
Nessun territorio in corso.
+
+
+ @endif From 2f0a24bca4e504457f416939019668606b454711 Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Mon, 19 Oct 2026 10:33:07 +0200 Subject: [PATCH 5/8] ++ campagne: "In corso" solo per territori assegnati nel periodo Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- app/Models/Campagna.php | 4 ++-- resources/views/livewire/campagne/campagna-show.blade.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 491e9c2..1c7ab3b 100644 --- a/README.md +++ b/README.md @@ -692,8 +692,8 @@ Al rientro la campagna viene proposta se il territorio era fuori almeno in parte del periodo: assegnato durante la campagna, rientrato durante la campagna, oppure fuori per tutta la sua durata. Se sia stato davvero percorso lo decide l'operatore con la spunta. Se più campagne corrispondono viene -proposta la più recente. I territori fuori durante la campagna e non ancora -rientrati compaiono nel dettaglio come "in corso". +proposta la più recente. I territori assegnati durante la campagna e non +ancora rientrati compaiono nel dettaglio come "in corso". --- diff --git a/app/Models/Campagna.php b/app/Models/Campagna.php index 121c9d3..1535886 100644 --- a/app/Models/Campagna.php +++ b/app/Models/Campagna.php @@ -61,12 +61,12 @@ class Campagna extends Model } /** - * Assignments still open that were out during this campaign (assigned - * on or before its end): they can be counted for it when they come back. + * Assignments made during this campaign and not yet returned. */ public function assegnazioniInCorso() { return Assegnazione::whereNull('returned_at') + ->where('assigned_at', '>=', $this->start_date) ->where('assigned_at', '<=', $this->end_date); } diff --git a/resources/views/livewire/campagne/campagna-show.blade.php b/resources/views/livewire/campagne/campagna-show.blade.php index fa0e779..5d07d8a 100644 --- a/resources/views/livewire/campagne/campagna-show.blade.php +++ b/resources/views/livewire/campagne/campagna-show.blade.php @@ -79,12 +79,12 @@ - {{-- Out during the campaign, not yet returned (hidden once a later campaign has started) --}} + {{-- Assigned during the campaign, not yet returned (hidden once a later campaign has started) --}} @if($inCorso !== null)

In corso ({{ $inCorso->count() }})

-

Fuori durante la campagna e non ancora rientrati: si potranno conteggiare al rientro.

+

Assegnati durante la campagna e non ancora rientrati: si potranno conteggiare al rientro.

From 40b4ca0b0b21353d6204d22a7a24fc2cf49bb9b1 Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Mon, 19 Oct 2026 10:37:37 +0200 Subject: [PATCH 6/8] ++ audit log: evento "campagna_percorso" per la scelta sul conteggio in campagna MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registrato al rientro quando viene proposta una campagna (Sì/No) e nel Registro quando l'amministratore imposta o modifica il conteggio. Co-Authored-By: Claude Opus 5.5 --- app/Livewire/Assegnazioni/Rientra.php | 10 ++++++++++ app/Livewire/Registro.php | 17 ++++++++++++++-- app/Models/Assegnazione.php | 21 ++++++++++++++++++++ resources/views/livewire/audit-log.blade.php | 1 + 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/app/Livewire/Assegnazioni/Rientra.php b/app/Livewire/Assegnazioni/Rientra.php index 430d6d3..bceec6a 100644 --- a/app/Livewire/Assegnazioni/Rientra.php +++ b/app/Livewire/Assegnazioni/Rientra.php @@ -98,6 +98,16 @@ class Rientra extends Component ]) ->log('returned'); + // The operator answered the campaign prompt: record the choice + // (counted or not) as its own audit entry. + if ($this->showCampaignPrompt && $this->campagna_id) { + $this->assegnazione->logCampagnaPercorso( + Campagna::find($this->campagna_id), + $this->counted_in_campaign, + 'Rientro' + ); + } + session()->flash('success', "Territorio {$territorio->numero} rientrato da {$proclamatore->nome_completo} dopo {$this->assegnazione->giorni} giorni."); return $this->redirect(route('territori.show', $territorio), navigate: true); } diff --git a/app/Livewire/Registro.php b/app/Livewire/Registro.php index 9245636..26fba29 100644 --- a/app/Livewire/Registro.php +++ b/app/Livewire/Registro.php @@ -141,10 +141,23 @@ class Registro extends Component ]; if ($this->editingId) { - Assegnazione::findOrFail($this->editingId)->update($data); + $assegnazione = Assegnazione::findOrFail($this->editingId); + $prima = [(bool) $assegnazione->counted_in_campaign, $assegnazione->campaign_id ? (int) $assegnazione->campaign_id : null]; + $assegnazione->update($data); } else { $data['created_by'] = auth()->id(); - Assegnazione::create($data); + $assegnazione = Assegnazione::create($data); + $prima = [false, null]; + } + + // Log the campaign choice when the admin sets or changes it here. + $dopo = [(bool) $assegnazione->counted_in_campaign, $assegnazione->campaign_id ? (int) $assegnazione->campaign_id : null]; + if ($dopo !== $prima && ($dopo[0] || $prima[0])) { + $assegnazione->logCampagnaPercorso( + $assegnazione->campagna ?? ($prima[1] ? Campagna::find($prima[1]) : null), + $dopo[0], + 'Registro' + ); } $this->showModal = false; diff --git a/app/Models/Assegnazione.php b/app/Models/Assegnazione.php index f3ad3a2..7175304 100644 --- a/app/Models/Assegnazione.php +++ b/app/Models/Assegnazione.php @@ -197,6 +197,27 @@ class Assegnazione extends Model // ─── Business Logic ───────────────────────────────────────── + /** + * Registra nell'audit log la scelta dell'operatore sul conteggio in + * campagna: evento dedicato "campagna_percorso", leggibile senza dover + * interpretare le proprietà dell'evento "returned". + */ + public function logCampagnaPercorso(?Campagna $campagna, bool $percorso, string $origine): void + { + $this->loadMissing(['territorio', 'proclamatore']); + + activity()->causedBy(auth()->user()) + ->performedOn($this) + ->withProperties([ + 'territorio' => $this->territorio?->numero, + 'proclamatore' => $this->proclamatore?->nome_completo, + 'campagna' => $campagna?->descrizione, + 'percorso_in_campagna' => $percorso ? 'Sì' : 'No', + 'origine' => $origine, + ]) + ->log('campagna_percorso'); + } + /** * Check if a campaign prompt should be shown when returning this assignment. * Returns the matching campaign or null. diff --git a/resources/views/livewire/audit-log.blade.php b/resources/views/livewire/audit-log.blade.php index 8c99f1b..8a921c4 100644 --- a/resources/views/livewire/audit-log.blade.php +++ b/resources/views/livewire/audit-log.blade.php @@ -49,6 +49,7 @@ 'deleted' => 'bg-red-100 text-red-800', 'assigned' => 'bg-indigo-100 text-indigo-800', 'returned' => 'bg-amber-100 text-amber-800', + 'campagna_percorso' => 'bg-orange-100 text-orange-800', 'restored' => 'bg-purple-100 text-purple-800', 'login' => 'bg-cyan-100 text-cyan-800', default => 'bg-gray-100 text-gray-700', From 358cbfc81c52aab6d3231e6c15885e5027871247 Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Mon, 19 Oct 2026 10:54:07 +0200 Subject: [PATCH 7/8] ++ rientro: avviso tempo di percorrenza elevato nel banner campagna MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se il territorio è rimasto fuori oltre la soglia "giorni_per_smarrito", il banner della campagna mostra un avviso (non bloccante). Giorni e avviso vengono salvati anche nell'evento "campagna_percorso". Co-Authored-By: Claude Opus 5.5 --- app/Livewire/Assegnazioni/Rientra.php | 11 +++++++++ app/Models/Assegnazione.php | 24 +++++++++++++++++++ .../livewire/assegnazioni/rientra.blade.php | 6 +++++ 3 files changed, 41 insertions(+) diff --git a/app/Livewire/Assegnazioni/Rientra.php b/app/Livewire/Assegnazioni/Rientra.php index bceec6a..11dc365 100644 --- a/app/Livewire/Assegnazioni/Rientra.php +++ b/app/Livewire/Assegnazioni/Rientra.php @@ -116,8 +116,19 @@ class Rientra extends Component { $campagna = $this->campagna_id ? Campagna::find($this->campagna_id) : null; + // Warning shown inside the campaign prompt only: holding time up to + // the selected return date above the "da rientrare" threshold. + $giorniPercorrenza = null; + $sogliaPercorrenza = null; + if ($campagna && $this->returned_at) { + $giorniPercorrenza = $this->assegnazione->giorniPercorrenza(\Carbon\Carbon::parse($this->returned_at)); + $sogliaPercorrenza = Assegnazione::sogliaPercorrenzaElevata(); + } + return view('livewire.assegnazioni.rientra', [ 'campagna' => $campagna, + 'giorniPercorrenza' => $giorniPercorrenza, + 'sogliaPercorrenza' => $sogliaPercorrenza, ]); } } diff --git a/app/Models/Assegnazione.php b/app/Models/Assegnazione.php index 7175304..8d9ae8f 100644 --- a/app/Models/Assegnazione.php +++ b/app/Models/Assegnazione.php @@ -206,6 +206,9 @@ class Assegnazione extends Model { $this->loadMissing(['territorio', 'proclamatore']); + $giorni = $this->giorniPercorrenza(); + $soglia = static::sogliaPercorrenzaElevata(); + activity()->causedBy(auth()->user()) ->performedOn($this) ->withProperties([ @@ -213,11 +216,32 @@ class Assegnazione extends Model 'proclamatore' => $this->proclamatore?->nome_completo, 'campagna' => $campagna?->descrizione, 'percorso_in_campagna' => $percorso ? 'Sì' : 'No', + 'giorni_percorrenza' => $giorni, + 'avviso_tempo_elevato' => $giorni > $soglia ? "Sì (soglia {$soglia} giorni)" : 'No', 'origine' => $origine, ]) ->log('campagna_percorso'); } + /** + * Giorni tra assegnazione e rientro (o la data indicata, o oggi). + */ + public function giorniPercorrenza(?Carbon $returnDate = null): int + { + $end = $returnDate ?? $this->returned_at ?? now(); + + return (int) Carbon::parse($this->assigned_at)->startOfDay()->diffInDays(Carbon::parse($end)->startOfDay()); + } + + /** + * Oltre questa soglia il tempo di percorrenza è considerato molto alto: + * è la stessa soglia "giorni_per_smarrito" dello stato "da_rientrare". + */ + public static function sogliaPercorrenzaElevata(): int + { + return (int) Setting::getValue('giorni_per_smarrito', 120); + } + /** * Check if a campaign prompt should be shown when returning this assignment. * Returns the matching campaign or null. diff --git a/resources/views/livewire/assegnazioni/rientra.blade.php b/resources/views/livewire/assegnazioni/rientra.blade.php index 79000ca..a25fd3c 100644 --- a/resources/views/livewire/assegnazioni/rientra.blade.php +++ b/resources/views/livewire/assegnazioni/rientra.blade.php @@ -47,6 +47,12 @@ ({{ $campagna->start_date->format('d/m/Y') }} — {{ $campagna->end_date->format('d/m/Y') }}). È stato percorso per la campagna? Se sì, conteggialo nella percentuale di percorrenza.

+ @if($giorniPercorrenza !== null && $giorniPercorrenza > $sogliaPercorrenza) +
+ ⚠️ Tempo di percorrenza molto alto: {{ $giorniPercorrenza }} giorni (soglia {{ $sogliaPercorrenza }}). + Verifica che il territorio sia stato effettivamente percorso durante la campagna. +
+ @endif
From bde7a4a8a854d1a05e983fd5569044080d7f3a15 Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Mon, 19 Oct 2026 12:33:20 +0200 Subject: [PATCH 8/8] ++ update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 62abd1c..a6872dd 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ docker-compose.override.yml db_data/ redis_data/ +.claude \ No newline at end of file