Implementate strategie avanzate e tracking aste v1.3.0
- Aggiunto BidStrategyService: adaptive latency, jitter, offset dinamico, heat metric, soft retreat, probabilistic bidding, profiling avversari, bankroll manager. - Esteso AuctionInfo con metriche avanzate: latenze, collisioni, heat, duello, tracking sessione, override strategie. - Nuova sezione "Strategie Avanzate" in Settings (UI) con opzioni dettagliate e bulk update. - Miglioramenti UX: auto-scroll log, filtri e dettagli avanzati in Statistics, gestione nomi prodotti, pulsanti sempre attivi. - Fix bug Blazor (layout, redirect, log, conteggio puntate, entità HTML). - Aggiornata documentazione, changelog, guide Docker/Gitea. - Versione incrementata a 1.3.0. Migrazione database per nuove metriche e tracking completo.
This commit is contained in:
@@ -11,11 +11,11 @@
|
|||||||
<DockerfileFile>Dockerfile</DockerfileFile>
|
<DockerfileFile>Dockerfile</DockerfileFile>
|
||||||
|
|
||||||
<!-- Versioning per Docker & Gitea Registry -->
|
<!-- Versioning per Docker & Gitea Registry -->
|
||||||
<!-- v1.1.0: Docker/Gitea publishing workflow + HTTPS fix -->
|
<!-- v1.3.0: Database management + bug fixes (duplicates, race conditions, warnings) -->
|
||||||
<Version>1.2.0</Version>
|
<Version>1.3.0</Version>
|
||||||
<AssemblyVersion>1.2.0.0</AssemblyVersion>
|
<AssemblyVersion>1.3.0.0</AssemblyVersion>
|
||||||
<FileVersion>1.2.0.0</FileVersion>
|
<FileVersion>1.3.0.0</FileVersion>
|
||||||
<InformationalVersion>1.2.0</InformationalVersion>
|
<InformationalVersion>1.3.0</InformationalVersion>
|
||||||
|
|
||||||
<!-- Metadata immagine Docker -->
|
<!-- Metadata immagine Docker -->
|
||||||
<ContainerImageName>autobidder</ContainerImageName>
|
<ContainerImageName>autobidder</ContainerImageName>
|
||||||
|
|||||||
@@ -1,767 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
Tutte le modifiche rilevanti a questo progetto saranno documentate in questo file.
|
|
||||||
|
|
||||||
Il formato è basato su [Keep a Changelog](https://keepachangelog.com/it/1.0.0/),
|
|
||||||
e questo progetto aderisce al [Semantic Versioning](https://semver.org/lang/it/).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.2.0] - 2025-01-18
|
|
||||||
|
|
||||||
### ?? Aggiunte (Added) - SICUREZZA
|
|
||||||
|
|
||||||
- **Sistema di autenticazione completo ASP.NET Core Identity**
|
|
||||||
- Login con username e password
|
|
||||||
- Protezione brute-force con lockout automatico (5 tentativi, 15 min block)
|
|
||||||
- Gestione sessioni sicura con cookie HttpOnly e SameSite
|
|
||||||
- Password policy forte (min 12 caratteri, maiuscole, minuscole, numeri, simboli)
|
|
||||||
|
|
||||||
- **Protezione route con autorizzazione**
|
|
||||||
- Tutte le pagine richiedono autenticazione
|
|
||||||
- Redirect automatico a `/login` per utenti non autenticati
|
|
||||||
- Pagina logout dedicata
|
|
||||||
|
|
||||||
- **Database Identity separato**
|
|
||||||
- SQLite per utenti e autenticazione
|
|
||||||
- Persistente su volume Docker `/app/Data`
|
|
||||||
- Inizializzazione automatica al primo avvio
|
|
||||||
|
|
||||||
- **Utente amministratore predefinito**
|
|
||||||
- Username configurabile via `ADMIN_USERNAME` (default: `admin`)
|
|
||||||
- Password obbligatoria via `ADMIN_PASSWORD` in production
|
|
||||||
- Password temporanea forte se non configurata: `Admin@Password123!`
|
|
||||||
- Warning nei log se usa password default
|
|
||||||
|
|
||||||
### ??? Modifiche (Changed) - SICUREZZA
|
|
||||||
|
|
||||||
- **Cookie di autenticazione sicuri**
|
|
||||||
- `HttpOnly=true` (protezione XSS)
|
|
||||||
- `SameSite=Lax` (protezione CSRF)
|
|
||||||
- `SecurePolicy=SameAsRequest` (compatibile Tailscale HTTP)
|
|
||||||
- Durata 7 giorni con sliding expiration
|
|
||||||
|
|
||||||
- **Configurazione Identity hardened**
|
|
||||||
- Lockout abilitato per nuovi utenti
|
|
||||||
- Timeout lockout: 15 minuti
|
|
||||||
- Max failed attempts: 5
|
|
||||||
- Password unique chars: 4
|
|
||||||
|
|
||||||
- **UI aggiornata con logout**
|
|
||||||
- Indicatore utente corrente in NavMenu
|
|
||||||
- Pulsante logout in sidebar
|
|
||||||
- Pagina login styled con gradiente
|
|
||||||
|
|
||||||
### ?? Note Tecniche
|
|
||||||
|
|
||||||
**Configurazione richiesta in `.env`:**
|
|
||||||
```bash
|
|
||||||
# Credenziali amministratore (OBBLIGATORIO!)
|
|
||||||
ADMIN_USERNAME=admin
|
|
||||||
ADMIN_PASSWORD=TuaPasswordSicura123!
|
|
||||||
```
|
|
||||||
|
|
||||||
**Password temporanea default:**
|
|
||||||
- Se `ADMIN_PASSWORD` non è settata, usa: `Admin@Password123!`
|
|
||||||
- ?? **CAMBIARE IMMEDIATAMENTE** dopo primo login!
|
|
||||||
- Viene mostrato warning nei log se usa password default
|
|
||||||
|
|
||||||
**Database:**
|
|
||||||
- Identity DB: `/app/Data/identity.db` (SQLite)
|
|
||||||
- Tabelle create automaticamente al primo avvio
|
|
||||||
- Utente admin creato se non esiste
|
|
||||||
|
|
||||||
**Sicurezza Tailscale:**
|
|
||||||
- Cookie `SecurePolicy=SameAsRequest` (funziona su HTTP Tailscale)
|
|
||||||
- Rate limiting brute-force integrato
|
|
||||||
- Session management ASP.NET Core
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
**PRIMA INSTALLAZIONE v1.2.0:**
|
|
||||||
1. Aggiungere `ADMIN_PASSWORD` al file `.env`
|
|
||||||
2. Riavviare container
|
|
||||||
3. Primo accesso con username/password configurati
|
|
||||||
4. (Opzionale) Cambiare password default se usata
|
|
||||||
|
|
||||||
**Aggiornamento da v1.1.x:**
|
|
||||||
- Primo avvio dopo aggiornamento creerà database Identity
|
|
||||||
- Se `ADMIN_PASSWORD` non settata, usa password temporanea
|
|
||||||
- ?? Cambiare password temporanea immediatamente!
|
|
||||||
|
|
||||||
### ?? Raccomandazioni Sicurezza
|
|
||||||
|
|
||||||
1. **Password forte obbligatoria:**
|
|
||||||
- Min 12 caratteri
|
|
||||||
- Maiuscole + minuscole
|
|
||||||
- Numeri
|
|
||||||
- Simboli speciali
|
|
||||||
- Esempio: `MyS3cur3P@ssw0rd!2024`
|
|
||||||
|
|
||||||
2. **Backup database Identity:**
|
|
||||||
```bash
|
|
||||||
docker cp AutoBidder:/app/Data/identity.db ./backup/
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Rotazione password periodica**
|
|
||||||
4. **Monitoraggio log accessi:**
|
|
||||||
```bash
|
|
||||||
docker logs AutoBidder | grep "\[Identity\]"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.1.2] - 2025-01-18
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
- **Fix critico: Container ascolta su porta 5000 invece di 8080**
|
|
||||||
- Forzato `UseUrls()` esplicito per garantire porta corretta
|
|
||||||
- Container ora ascolta definitivamente su porta 8080
|
|
||||||
- Healthcheck ora passa correttamente
|
|
||||||
- Applicazione web accessibile correttamente
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
- **Program.cs: Forzata porta con `UseUrls()`**
|
|
||||||
- Aggiunto controllo esplicito ASPNETCORE_URLS all'avvio
|
|
||||||
- Garantisce che nessuna configurazione sovrascriva la porta
|
|
||||||
- Log più chiaro della porta in ascolto
|
|
||||||
|
|
||||||
- **Dockerfile: Healthcheck migliorato**
|
|
||||||
- Timeout aumentato a 30s (da 10s)
|
|
||||||
- Start period aumentato a 90s (da 40s)
|
|
||||||
- Retries aumentati a 5 (da 3)
|
|
||||||
- Più tempo per Blazor Server per avviarsi completamente
|
|
||||||
|
|
||||||
### ?? Note Tecniche
|
|
||||||
|
|
||||||
**Problema:**
|
|
||||||
- Container continuava ad ascoltare su porta 5000 invece di 8080
|
|
||||||
- Healthcheck falliva: `curl: (7) Failed to connect to localhost port 8080`
|
|
||||||
- Log mostrava: `Now listening on: http://[::]:5000`
|
|
||||||
|
|
||||||
**Root Cause:**
|
|
||||||
- Configurazioni di default .NET sovra scrivevano `ASPNETCORE_URLS`
|
|
||||||
- `launchSettings.json` poteva influenzare il comportamento
|
|
||||||
|
|
||||||
**Soluzione:**
|
|
||||||
- Forzato `builder.WebHost.UseUrls()` esplicitamente nel Program.cs
|
|
||||||
- Garantisce precedenza assoluta sulla porta configurata
|
|
||||||
- Healthcheck aggiornato per Blazor Server (tempi più lunghi)
|
|
||||||
|
|
||||||
|
|
||||||
## [1.2.0] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.1.1] - 2025-01-18
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
- **Fix critico: Container in ascolto su porta sbagliata**
|
|
||||||
- Container ora ascolta correttamente sulla porta 8080 (configurata in ASPNETCORE_URLS)
|
|
||||||
- Rimossa configurazione esplicita HTTP in Program.cs che causava conflitti
|
|
||||||
- Kestrel ora rispetta ASPNETCORE_URLS per la porta HTTP
|
|
||||||
- Pagina web ora carica correttamente quando si accede al container
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
- **Configurazione Kestrel semplificata**
|
|
||||||
- HTTP gestito esclusivamente da ASPNETCORE_URLS
|
|
||||||
- Configurazione Kestrel utilizzata solo per HTTPS opzionale
|
|
||||||
- Log migliorato per mostrare porta di ascolto
|
|
||||||
|
|
||||||
### ?? Note Tecniche
|
|
||||||
|
|
||||||
**Problema:** Container ascoltava su porta 5000 invece di 8080, causando pagina che non caricava.
|
|
||||||
|
|
||||||
**Causa:** Conflitto tra configurazione esplicita `options.ListenAnyIP(8080)` e impostazioni default Kestrel.
|
|
||||||
|
|
||||||
**Soluzione:** Rimossa configurazione esplicita HTTP, ASPNETCORE_URLS ora gestisce tutto.
|
|
||||||
|
|
||||||
|
|
||||||
## [1.1.2] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
|
|
||||||
## [1.2.0] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.2.0] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.1.0] - 2025-01-18
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
- **Pubblicazione automatica su Gitea Container Registry**
|
|
||||||
- Workflow integrato con Visual Studio (tasto destro ? Pubblica)
|
|
||||||
- Versionamento automatico da `<Version>` del `.csproj`
|
|
||||||
- Tag multipli: `latest` + versione specifica (es. `1.1.0`)
|
|
||||||
- Post-build target per push automatico su Gitea
|
|
||||||
|
|
||||||
- **Profilo di pubblicazione `GiteaRegistry.pubxml`**
|
|
||||||
- Profilo custom senza dipendenze Docker SDK
|
|
||||||
- Target `DockerBuild` integrato
|
|
||||||
- Build e push automatici in un solo comando
|
|
||||||
|
|
||||||
- **Documentazione completa Docker/Gitea**
|
|
||||||
- `DOCKER_PUBLISH_GUIDE.md`: Guida pubblicazione passo-passo
|
|
||||||
- `CONFIGURAZIONE_FINALE.md`: Riepilogo configurazione
|
|
||||||
- `PROBLEMA_RISOLTO.md`: Troubleshooting Visual Studio
|
|
||||||
- `PROBLEMA_HTTPS_RISOLTO.md`: Fix container HTTPS
|
|
||||||
- `RIEPILOGO_COMPLETO_FINALE.md`: Overview completa
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
- **Porta HTTP container: `5000` ? `8080`**
|
|
||||||
- Porta standard per container HTTP
|
|
||||||
- Compatibile con convenzioni Docker/Kubernetes
|
|
||||||
|
|
||||||
- **HTTPS disabilitato di default in container**
|
|
||||||
- `Kestrel__EnableHttps=false` nel Dockerfile
|
|
||||||
- HTTPS gestito da reverse proxy in production
|
|
||||||
- Certificati opzionali per chi ne ha bisogno
|
|
||||||
|
|
||||||
- **Convenzione path Gitea Registry corretta**
|
|
||||||
- Da: `gitea.encke-hake.ts.net/alby96/mimante/autobidder` (4 livelli - errato)
|
|
||||||
- A: `gitea.encke-hake.ts.net/alby96/autobidder` (3 livelli - corretto)
|
|
||||||
- Conforme a standard Gitea `{registry}/{owner}/{image}`
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
- **Errore Visual Studio "ContainerBuild target not found"**
|
|
||||||
- Profilo cambiato da `WebPublishMethod=Docker` a `Custom`
|
|
||||||
- Rimossa dipendenza da Microsoft.Docker.Sdk non installato
|
|
||||||
- Visual Studio ora mostra SUCCESS senza errori
|
|
||||||
|
|
||||||
- **Crash container all'avvio per certificati HTTPS**
|
|
||||||
- Kestrel non cerca più certificati di sviluppo inesistenti
|
|
||||||
- Container si avvia correttamente in modalità HTTP-only
|
|
||||||
- HTTPS abilitabile manualmente con certificato fornito
|
|
||||||
|
|
||||||
- **Push Gitea falliva silenziosamente**
|
|
||||||
- Workflow ora completamente automatico e tracciabile
|
|
||||||
- Output dettagliato con conferma digest SHA256
|
|
||||||
- Link diretto al package pubblicato
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
- Profilo `GiteaRegistry-LocalOnly.pubxml` (ridondante)
|
|
||||||
- Dipendenza implicita da certificati HTTPS in Development
|
|
||||||
|
|
||||||
### ?? Sicurezza (Security)
|
|
||||||
|
|
||||||
- Gestione corretta certificati SSL/TLS
|
|
||||||
- HTTPS opzionale invece che obbligatorio
|
|
||||||
- Reverse proxy consigliato per terminazione SSL
|
|
||||||
|
|
||||||
### ?? Note di Migrazione
|
|
||||||
|
|
||||||
**Breaking Changes:**
|
|
||||||
|
|
||||||
1. **Porta HTTP cambiata**
|
|
||||||
- Se usavi `5000:5000`, ora usa `5000:8080`
|
|
||||||
- Docker Compose: aggiornare port mapping
|
|
||||||
- Unraid: modificare configurazione porta container
|
|
||||||
|
|
||||||
2. **HTTPS disabilitato**
|
|
||||||
- Se usavi HTTPS diretto, configura reverse proxy
|
|
||||||
- Oppure abilita manualmente con certificato:
|
|
||||||
```bash
|
|
||||||
-e Kestrel__EnableHttps=true
|
|
||||||
-e Kestrel__Certificates__Default__Path=/certs/cert.pfx
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Path Gitea cambiato**
|
|
||||||
- Le vecchie immagini `alby96/mimante/autobidder` rimangono disponibili
|
|
||||||
- Nuove immagini: `alby96/autobidder`
|
|
||||||
- Aggiornare pull command nei deployment
|
|
||||||
|
|
||||||
**Aggiornamento consigliato:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Pull nuova versione
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.1.0
|
|
||||||
|
|
||||||
# Stop vecchio container
|
|
||||||
docker stop autobidder
|
|
||||||
docker rm autobidder
|
|
||||||
|
|
||||||
# Avvia nuovo container con porta corretta
|
|
||||||
docker run -d \
|
|
||||||
--name autobidder \
|
|
||||||
-p 5000:8080 \
|
|
||||||
-v /data:/app/Data \
|
|
||||||
gitea.encke-hake.ts.net/alby96/autobidder:1.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## [1.1.1] - 2026-01-20
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
|
|
||||||
## [1.1.2] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
|
|
||||||
## [1.2.0] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.2.0] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.1.2] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
|
|
||||||
## [1.2.0] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.2.0] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.0.0] - 2025-01-17
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
- Release iniziale sistema AutoBidder
|
|
||||||
- Interfaccia Blazor Server .NET 8
|
|
||||||
- Monitoraggio aste Bidoo in tempo reale
|
|
||||||
- Sistema di offerte automatiche
|
|
||||||
- Statistiche avanzate con PostgreSQL
|
|
||||||
- Backup database automatici
|
|
||||||
- Docker support di base
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
- N/A (prima release)
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
- N/A (prima release)
|
|
||||||
|
|
||||||
|
|
||||||
## [1.1.1] - 2026-01-20
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
|
|
||||||
## [1.1.2] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
|
|
||||||
## [1.2.0] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.2.0] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.1.2] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
|
|
||||||
## [1.2.0] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.2.0] - 2026-01-21
|
|
||||||
|
|
||||||
### ? Aggiunte (Added)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Modifiche (Changed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Correzioni (Fixed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ??? Rimossi (Removed)
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### ?? Breaking Changes
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tipologie di Modifiche
|
|
||||||
|
|
||||||
- `? Aggiunte (Added)`: Nuove funzionalità
|
|
||||||
- `?? Modifiche (Changed)`: Modifiche a funzionalità esistenti
|
|
||||||
- `??? Rimossi (Removed)`: Funzionalità rimosse
|
|
||||||
- `?? Correzioni (Fixed)`: Bug fix
|
|
||||||
- `?? Sicurezza (Security)`: Fix di sicurezza
|
|
||||||
- `?? Deprecati (Deprecated)`: Funzionalità obsolete (da rimuovere)
|
|
||||||
|
|
||||||
## Versioning
|
|
||||||
|
|
||||||
Questo progetto segue [Semantic Versioning](https://semver.org/lang/it/):
|
|
||||||
|
|
||||||
- **MAJOR** (1.x.x ? 2.x.x): Breaking changes incompatibili
|
|
||||||
- **MINOR** (x.1.x ? x.2.x): Nuove feature retrocompatibili
|
|
||||||
- **PATCH** (x.x.1 ? x.x.2): Bug fix retrocompatibili
|
|
||||||
|
|
||||||
Esempi:
|
|
||||||
- `1.0.0` ? `1.1.0`: Nuova feature (Gitea publishing)
|
|
||||||
- `1.1.0` ? `1.1.1`: Bug fix
|
|
||||||
- `1.1.0` ? `2.0.0`: Breaking change (API cambiate)
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
# ?? CONFIGURAZIONE FINALE - UN SOLO PROFILO
|
|
||||||
|
|
||||||
## ? Cosa è Cambiato
|
|
||||||
|
|
||||||
### PRIMA (Configurazione Complessa)
|
|
||||||
- ? Due profili: `GiteaRegistry` e `GiteaRegistry-LocalOnly`
|
|
||||||
- ? Versionamento manuale
|
|
||||||
- ? Confusione su quale profilo usare
|
|
||||||
|
|
||||||
### DOPO (Configurazione Semplificata)
|
|
||||||
- ? **UN SOLO PROFILO**: `GiteaRegistry.pubxml`
|
|
||||||
- ? **Versionamento automatico** da `<Version>` della solution
|
|
||||||
- ? **Workflow chiaro** e lineare
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Struttura Files
|
|
||||||
|
|
||||||
```
|
|
||||||
AutoBidder/
|
|
||||||
??? AutoBidder.csproj
|
|
||||||
? ??? <Version>1.0.0</Version> ? VERSIONE SOLUTION (fonte unica)
|
|
||||||
? ??? <Target Name="PushDockerImageToGitea"> ? Post-build automatico
|
|
||||||
??? Dockerfile ? Build immagine Docker
|
|
||||||
??? Properties/
|
|
||||||
? ??? PublishProfiles/
|
|
||||||
? ??? GiteaRegistry.pubxml ? UNICO PROFILO (tutto automatico)
|
|
||||||
??? DOCKER_PUBLISH_GUIDE.md ? Guida aggiornata
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Come Funziona
|
|
||||||
|
|
||||||
### 1. Definisci Versione Solution
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- AutoBidder.csproj -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<Version>1.0.1</Version> ? Modifica qui per nuova versione
|
|
||||||
</PropertyGroup>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Pubblica da Visual Studio
|
|
||||||
|
|
||||||
```
|
|
||||||
Tasto destro progetto ? Pubblica ? GiteaRegistry ? Pubblica
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Sistema Automatico
|
|
||||||
|
|
||||||
```
|
|
||||||
???????????????????????????????????
|
|
||||||
? Visual Studio: Publish ?
|
|
||||||
???????????????????????????????????
|
|
||||||
?
|
|
||||||
?
|
|
||||||
???????????????????????????????????
|
|
||||||
? Build .NET (Release) ?
|
|
||||||
???????????????????????????????????
|
|
||||||
?
|
|
||||||
?
|
|
||||||
???????????????????????????????????
|
|
||||||
? Docker build ?
|
|
||||||
? ? autobidder:latest ?
|
|
||||||
???????????????????????????????????
|
|
||||||
?
|
|
||||||
?
|
|
||||||
???????????????????????????????????
|
|
||||||
? POST-BUILD (AutoBidder.csproj) ?
|
|
||||||
? ?
|
|
||||||
? Legge: <Version>1.0.1</Version> ?
|
|
||||||
? ?
|
|
||||||
? Tag: ?
|
|
||||||
? • autobidder:latest ?
|
|
||||||
? ? gitea.../alby96/ ?
|
|
||||||
? autobidder:latest ?
|
|
||||||
? ?
|
|
||||||
? • autobidder:latest ?
|
|
||||||
? ? gitea.../alby96/ ?
|
|
||||||
? autobidder:1.0.1 ?
|
|
||||||
???????????????????????????????????
|
|
||||||
?
|
|
||||||
?
|
|
||||||
???????????????????????????????????
|
|
||||||
? Push su Gitea ?
|
|
||||||
? ?
|
|
||||||
? ? latest (aggiornato) ?
|
|
||||||
? ? 1.0.1 (nuovo tag) ?
|
|
||||||
???????????????????????????????????
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Versionamento Automatico
|
|
||||||
|
|
||||||
### Source of Truth
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- AutoBidder.csproj - UNICA FONTE DI VERITÀ -->
|
|
||||||
<Version>1.0.1</Version>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tag Generati Automaticamente
|
|
||||||
|
|
||||||
| Versione Solution | Tag Latest | Tag Versione | Nota |
|
|
||||||
|-------------------|------------|--------------|------|
|
|
||||||
| `1.0.0` | `:latest` ? 1.0.0 | `:1.0.0` | Prima versione |
|
|
||||||
| `1.0.1` | `:latest` ? 1.0.1 | `:1.0.1` + `:1.0.0` rimane | Latest aggiornato |
|
|
||||||
| `2.0.0` | `:latest` ? 2.0.0 | `:2.0.0` + precedenti | Major update |
|
|
||||||
|
|
||||||
### Storico Versioni su Gitea
|
|
||||||
|
|
||||||
Gitea mantiene **TUTTI i tag** pubblicati:
|
|
||||||
|
|
||||||
```
|
|
||||||
?? gitea.encke-hake.ts.net/alby96/autobidder
|
|
||||||
??? ??? latest (? 1.0.1) [sempre aggiornato]
|
|
||||||
??? ??? 1.0.1 [immutabile]
|
|
||||||
??? ??? 1.0.0 [immutabile]
|
|
||||||
??? ??? 0.9.0 [immutabile]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Esempio Pratico: Rilascio Versione 1.0.2
|
|
||||||
|
|
||||||
### Step 1: Aggiorna Versione
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- AutoBidder.csproj -->
|
|
||||||
<Version>1.0.2</Version> <!-- Era 1.0.1 -->
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Pubblica
|
|
||||||
|
|
||||||
1. Tasto destro ? Pubblica
|
|
||||||
2. Seleziona `GiteaRegistry`
|
|
||||||
3. Click **Pubblica**
|
|
||||||
|
|
||||||
### Step 3: Output Automatico
|
|
||||||
|
|
||||||
```
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
? POST-BUILD: Pubblicazione su Gitea Container Registry ?
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
|
|
||||||
?? Solution Version: 1.0.2
|
|
||||||
??? Target Tags:
|
|
||||||
• gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
• gitea.encke-hake.ts.net/alby96/autobidder:1.0.2
|
|
||||||
|
|
||||||
? Tagged: gitea.../autobidder:latest
|
|
||||||
? Tagged: gitea.../autobidder:1.0.2
|
|
||||||
|
|
||||||
? Pushed: gitea.../autobidder:latest
|
|
||||||
? Pushed: gitea.../autobidder:1.0.2
|
|
||||||
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
? ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ?
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
|
|
||||||
?? Tag pubblicati:
|
|
||||||
• latest (ora punta a 1.0.2)
|
|
||||||
• 1.0.2 (nuova versione)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Verifica su Gitea
|
|
||||||
|
|
||||||
```
|
|
||||||
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder
|
|
||||||
```
|
|
||||||
|
|
||||||
Vedrai:
|
|
||||||
- `latest` ? digest aggiornato (ora è 1.0.2)
|
|
||||||
- `1.0.2` ? nuovo tag creato
|
|
||||||
- `1.0.1` ? ancora disponibile
|
|
||||||
- `1.0.0` ? ancora disponibile
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Vantaggi del Nuovo Sistema
|
|
||||||
|
|
||||||
| Aspetto | Prima | Dopo |
|
|
||||||
|---------|-------|------|
|
|
||||||
| **Profili** | 2 (confusione) | 1 (chiaro) |
|
|
||||||
| **Versionamento** | Manuale | Automatico |
|
|
||||||
| **Source of Truth** | Multipli | Unico (`<Version>`) |
|
|
||||||
| **Complessità** | Alta | Bassa |
|
|
||||||
| **Errori** | Facili | Difficili |
|
|
||||||
| **Manutenibilità** | Difficile | Facile |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Best Practices
|
|
||||||
|
|
||||||
### 1. Semantic Versioning
|
|
||||||
|
|
||||||
Segui il formato: `MAJOR.MINOR.PATCH`
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- Esempi -->
|
|
||||||
<Version>1.0.0</Version> ? Release iniziale
|
|
||||||
<Version>1.0.1</Version> ? Bug fix
|
|
||||||
<Version>1.1.0</Version> ? Nuova feature
|
|
||||||
<Version>2.0.0</Version> ? Breaking change
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Deploy Production
|
|
||||||
|
|
||||||
**? MAI usare `latest` in production:**
|
|
||||||
```yaml
|
|
||||||
# ERRATO
|
|
||||||
image: gitea.../autobidder:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
**? USA sempre versione specifica:**
|
|
||||||
```yaml
|
|
||||||
# CORRETTO
|
|
||||||
image: gitea.../autobidder:1.0.2
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Testing
|
|
||||||
|
|
||||||
Prima di deployare in production:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Pull versione specifica
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.0.2
|
|
||||||
|
|
||||||
# 2. Test locale
|
|
||||||
docker run -p 5000:8080 gitea.../autobidder:1.0.2
|
|
||||||
|
|
||||||
# 3. Verifica funzionalità
|
|
||||||
# http://localhost:5000
|
|
||||||
|
|
||||||
# 4. Se OK ? Deploy production
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Changelog
|
|
||||||
|
|
||||||
Mantieni un file `CHANGELOG.md` nella repo:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# Changelog
|
|
||||||
|
|
||||||
## [1.0.2] - 2026-01-18
|
|
||||||
### Fixed
|
|
||||||
- Correzione bug autenticazione Gitea
|
|
||||||
|
|
||||||
## [1.0.1] - 2026-01-17
|
|
||||||
### Added
|
|
||||||
- Supporto versionamento automatico
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Comandi Rapidi
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Autenticazione (prima volta)
|
|
||||||
docker login gitea.encke-hake.ts.net
|
|
||||||
|
|
||||||
# Pubblica da Visual Studio
|
|
||||||
# Tasto destro ? Pubblica ? GiteaRegistry
|
|
||||||
|
|
||||||
# Pull versione specifica (production)
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.0.2
|
|
||||||
|
|
||||||
# Pull latest (development)
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
|
|
||||||
# Lista tutti i tag disponibili (via API)
|
|
||||||
curl https://gitea.encke-hake.ts.net/api/v1/packages/Alby96/container/autobidder
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Finali
|
|
||||||
|
|
||||||
| File | Scopo |
|
|
||||||
|------|-------|
|
|
||||||
| `AutoBidder.csproj` | Versione solution + post-build target |
|
|
||||||
| `Properties/PublishProfiles/GiteaRegistry.pubxml` | UNICO profilo pubblicazione |
|
|
||||||
| `Dockerfile` | Build immagine Docker |
|
|
||||||
| `.dockerignore` | Esclusioni Docker |
|
|
||||||
| `DOCKER_PUBLISH_GUIDE.md` | Guida utente completa |
|
|
||||||
| `VERIFICA_CONFIGURAZIONE_GITEA.md` | Checklist conformità |
|
|
||||||
| `NUOVO_WORKFLOW_RIEPILOGO.md` | Dettagli tecnici workflow |
|
|
||||||
| **`CONFIGURAZIONE_FINALE.md`** | **Questo documento** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**? CONFIGURAZIONE COMPLETATA E SEMPLIFICATA!**
|
|
||||||
|
|
||||||
Ora hai un sistema **professionale**, **automatico** e **tracciabile** per gestire versioni Docker su Gitea! ??
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
# ?? AutoBidder - Docker Deploy su Gitea
|
|
||||||
|
|
||||||
Setup minimalista per build e deploy Docker.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Requisiti
|
|
||||||
|
|
||||||
- Docker Desktop running
|
|
||||||
- Login Gitea Registry:
|
|
||||||
```powershell
|
|
||||||
docker login gitea.encke-hake.ts.net
|
|
||||||
# Username: alby96
|
|
||||||
# Password: <personal-access-token>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Genera token**: https://gitea.encke-hake.ts.net/user/settings/applications ? Permissions: `write:packages`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Publish da Visual Studio
|
|
||||||
|
|
||||||
```
|
|
||||||
Build ? Publish ? Docker ? Publish
|
|
||||||
```
|
|
||||||
|
|
||||||
**Automatico**:
|
|
||||||
- Build immagine Docker
|
|
||||||
- Tag: `latest`, `1.0.0`, `1.0.0-20260118`
|
|
||||||
- Push su Gitea Registry
|
|
||||||
|
|
||||||
**Registry**: https://gitea.encke-hake.ts.net/alby96/mimante/-/packages/container/autobidder
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Aggiornare Versione
|
|
||||||
|
|
||||||
Modifica `AutoBidder.csproj`:
|
|
||||||
```xml
|
|
||||||
<PropertyGroup>
|
|
||||||
<Version>1.0.1</Version>
|
|
||||||
</PropertyGroup>
|
|
||||||
```
|
|
||||||
|
|
||||||
Poi publish come sopra.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Deploy Unraid
|
|
||||||
|
|
||||||
### Via Template
|
|
||||||
|
|
||||||
1. Unraid ? Docker ? Add Template
|
|
||||||
2. URL: `https://192.168.30.23/Alby96/Mimante/raw/branch/docker/deployment/unraid-template.xml`
|
|
||||||
3. Install "AutoBidder"
|
|
||||||
4. Configura:
|
|
||||||
- Port: `8888:8080`
|
|
||||||
- AppData: `/mnt/user/appdata/autobidder`
|
|
||||||
- PostgreSQL: `Host=192.168.30.23;Port=5432;...`
|
|
||||||
5. Apply
|
|
||||||
|
|
||||||
### Via Docker Compose
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Accesso: http://localhost:8080
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Troubleshooting
|
|
||||||
|
|
||||||
### Publish fallisce: "unauthorized"
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
docker login gitea.encke-hake.ts.net
|
|
||||||
# Retry publish
|
|
||||||
```
|
|
||||||
|
|
||||||
### Container non parte
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Verifica porta libera
|
|
||||||
netstat -ano | findstr :8080
|
|
||||||
|
|
||||||
# Rebuild
|
|
||||||
docker build -t test .
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Configurazione
|
|
||||||
|
|
||||||
| File | Scopo |
|
|
||||||
|------|-------|
|
|
||||||
| `Dockerfile` | Build immagine multi-stage |
|
|
||||||
| `docker-compose.yml` | Deploy con PostgreSQL |
|
|
||||||
| `Properties/PublishProfiles/Docker.pubxml` | Profilo publish Visual Studio |
|
|
||||||
| `deployment/unraid-template.xml` | Template Unraid |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Setup completo! Build ? Publish ? Docker per deployare! ??**
|
|
||||||
@@ -1,505 +0,0 @@
|
|||||||
# Guida Pubblicazione Docker su Gitea Registry
|
|
||||||
|
|
||||||
Questa guida spiega come pubblicare l'immagine Docker di AutoBidder sul registry Gitea usando il **nuovo workflow integrato con Visual Studio**.
|
|
||||||
|
|
||||||
## Prerequisiti
|
|
||||||
|
|
||||||
1. **Docker installato e in esecuzione**
|
|
||||||
2. **Accesso al registry Gitea**: `gitea.encke-hake.ts.net`
|
|
||||||
3. **Token PAT** (Personal Access Token) con permessi `read:packages` e `write:packages`
|
|
||||||
|
|
||||||
## 1. Autenticazione con Gitea (OBBLIGATORIA)
|
|
||||||
|
|
||||||
Prima di pubblicare, devi autenticarti con il registry Gitea usando un **Token PAT**:
|
|
||||||
|
|
||||||
### Genera Token PAT
|
|
||||||
|
|
||||||
1. Vai su: `https://gitea.encke-hake.ts.net/user/settings/applications`
|
|
||||||
2. Click **Generate New Token**
|
|
||||||
3. Seleziona scope: **`read:packages`** + **`write:packages`**
|
|
||||||
4. Copia il token generato
|
|
||||||
|
|
||||||
### Autentica Docker
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker login gitea.encke-hake.ts.net
|
|
||||||
# Username: Alby96
|
|
||||||
# Password: [INCOLLA IL TOKEN PAT QUI]
|
|
||||||
```
|
|
||||||
|
|
||||||
**IMPORTANTE:** Se hai 2FA attivo su Gitea, il Token PAT è **OBBLIGATORIO** (la password normale non funziona).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Guida Pubblicazione Docker su Gitea Registry
|
|
||||||
|
|
||||||
Questa guida spiega come pubblicare l'immagine Docker di AutoBidder sul registry Gitea con **versionamento automatico** basato sulla solution.
|
|
||||||
|
|
||||||
## Prerequisiti
|
|
||||||
|
|
||||||
1. **Docker Desktop** installato e in esecuzione
|
|
||||||
2. **Accesso al registry Gitea**: `gitea.encke-hake.ts.net`
|
|
||||||
3. **Token PAT** (Personal Access Token) con permessi `read:packages` e `write:packages`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Autenticazione con Gitea (OBBLIGATORIA - Una Volta)
|
|
||||||
|
|
||||||
### Genera Token PAT
|
|
||||||
|
|
||||||
1. Vai su: `https://gitea.encke-hake.ts.net/user/settings/applications`
|
|
||||||
2. Click **Generate New Token**
|
|
||||||
3. Nome: `Docker Registry Access`
|
|
||||||
4. Seleziona scope: **`read:packages`** + **`write:packages`**
|
|
||||||
5. Click **Generate Token**
|
|
||||||
6. **Copia il token** (non sarà più visibile!)
|
|
||||||
|
|
||||||
### Autentica Docker
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker login gitea.encke-hake.ts.net
|
|
||||||
# Username: Alby96
|
|
||||||
# Password: [INCOLLA IL TOKEN PAT]
|
|
||||||
```
|
|
||||||
|
|
||||||
**? Success:** `Login Succeeded`
|
|
||||||
|
|
||||||
**?? IMPORTANTE:** Con 2FA attivo su Gitea, il Token PAT è **OBBLIGATORIO** (la password normale non funziona).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Pubblicare su Gitea con Versionamento Automatico
|
|
||||||
|
|
||||||
### ?? Workflow Completo in 3 Step
|
|
||||||
|
|
||||||
#### Step 1: Aggiorna Versione Solution (Opzionale)
|
|
||||||
|
|
||||||
Apri `AutoBidder.csproj` e modifica:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<Version>1.0.1</Version> <!-- Incrementa la versione -->
|
|
||||||
```
|
|
||||||
|
|
||||||
La versione qui definita sarà usata per taggare l'immagine Docker.
|
|
||||||
|
|
||||||
#### Step 2: Pubblica da Visual Studio
|
|
||||||
|
|
||||||
1. **Tasto destro** sul progetto `AutoBidder`
|
|
||||||
2. Seleziona **Pubblica**
|
|
||||||
3. Scegli il profilo: **`GiteaRegistry`** (UNICO profilo disponibile)
|
|
||||||
4. Click **Pubblica**
|
|
||||||
|
|
||||||
#### Step 3: Verifica Pubblicazione
|
|
||||||
|
|
||||||
Il sistema mostrerà output dettagliato:
|
|
||||||
|
|
||||||
```
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
? POST-BUILD: Pubblicazione su Gitea Container Registry ?
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
|
|
||||||
?? Solution Version: 1.0.1
|
|
||||||
?? Local Image: autobidder:latest
|
|
||||||
??? Target Tags:
|
|
||||||
• gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
• gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
|
|
||||||
|
|
||||||
???????????????????????????????????????????????????????????????????
|
|
||||||
??? Tagging images...
|
|
||||||
???????????????????????????????????????????????????????????????????
|
|
||||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
|
|
||||||
|
|
||||||
???????????????????????????????????????????????????????????????????
|
|
||||||
?? Pushing to Gitea Registry...
|
|
||||||
???????????????????????????????????????????????????????????????????
|
|
||||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
|
|
||||||
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
? ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ?
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Sistema di Versionamento
|
|
||||||
|
|
||||||
### Come Funziona
|
|
||||||
|
|
||||||
Il versionamento è **completamente automatico** e basato su:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- AutoBidder.csproj -->
|
|
||||||
<Version>1.0.1</Version>
|
|
||||||
```
|
|
||||||
|
|
||||||
Quando pubblichi:
|
|
||||||
- ? Tag `latest` ? **sempre aggiornato** all'ultima versione
|
|
||||||
- ? Tag `1.0.1` ? **versione specifica** immutabile
|
|
||||||
|
|
||||||
### Esempi Pratici
|
|
||||||
|
|
||||||
**Scenario 1: Prima pubblicazione**
|
|
||||||
```xml
|
|
||||||
<Version>1.0.0</Version>
|
|
||||||
```
|
|
||||||
Risultato:
|
|
||||||
- `gitea.../alby96/autobidder:latest` ? v1.0.0
|
|
||||||
- `gitea.../alby96/autobidder:1.0.0` ? v1.0.0
|
|
||||||
|
|
||||||
**Scenario 2: Aggiornamento versione**
|
|
||||||
```xml
|
|
||||||
<Version>1.0.1</Version>
|
|
||||||
```
|
|
||||||
Risultato:
|
|
||||||
- `gitea.../alby96/autobidder:latest` ? **aggiornato** a v1.0.1
|
|
||||||
- `gitea.../alby96/autobidder:1.0.1` ? **nuovo tag** creato
|
|
||||||
- `gitea.../alby96/autobidder:1.0.0` ? rimane disponibile
|
|
||||||
|
|
||||||
### Best Practices
|
|
||||||
|
|
||||||
| Ambiente | Tag Consigliato | Motivo |
|
|
||||||
|----------|----------------|---------|
|
|
||||||
| **Development** | `latest` | Sempre l'ultima versione |
|
|
||||||
| **Staging** | `1.0.1` | Versione specifica per test |
|
|
||||||
| **Production** | `1.0.1` | Versione immutabile e tracciabile |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Dove Trovare le Immagini Pubblicate
|
|
||||||
|
|
||||||
### Link Diretto al Package
|
|
||||||
```
|
|
||||||
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lista Packages Utente
|
|
||||||
```
|
|
||||||
https://gitea.encke-hake.ts.net/Alby96/-/packages
|
|
||||||
```
|
|
||||||
|
|
||||||
Su Gitea vedrai:
|
|
||||||
- ?? Nome: **`autobidder`**
|
|
||||||
- ??? Tag: `latest`, `1.0.0`, `1.0.1`, ...
|
|
||||||
- ?? Data pubblicazione
|
|
||||||
- ?? Digest SHA256
|
|
||||||
- ?? Dimensione immagine
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Usare l'Immagine Pubblicata
|
|
||||||
|
|
||||||
### Pull con Versione Specifica
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Versione immutabile (CONSIGLIATO per production)
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
|
|
||||||
|
|
||||||
# Latest (sempre aggiornato)
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Su Unraid
|
|
||||||
|
|
||||||
1. Docker tab ? **Add Container**
|
|
||||||
2. **Repository**: `gitea.encke-hake.ts.net/alby96/autobidder:1.0.1`
|
|
||||||
3. **Port**: `5000` ? `8080`
|
|
||||||
4. **Volume 1**: `/mnt/user/appdata/autobidder/data` ? `/app/Data`
|
|
||||||
5. **Volume 2**: `/mnt/user/appdata/autobidder/logs` ? `/app/logs`
|
|
||||||
6. **Environment**: `ASPNETCORE_ENVIRONMENT=Production`
|
|
||||||
7. **Restart**: `unless-stopped`
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
autobidder:
|
|
||||||
image: gitea.encke-hake.ts.net/alby96/autobidder:1.0.1 # Versione specifica
|
|
||||||
container_name: autobidder
|
|
||||||
ports:
|
|
||||||
- "5000:8080"
|
|
||||||
volumes:
|
|
||||||
- ./data:/app/Data
|
|
||||||
- ./logs:/app/logs
|
|
||||||
environment:
|
|
||||||
- ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
restart: unless-stopped
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name autobidder \
|
|
||||||
-p 5000:8080 \
|
|
||||||
-v /path/to/data:/app/Data \
|
|
||||||
-v /path/to/logs:/app/logs \
|
|
||||||
-e ASPNETCORE_ENVIRONMENT=Production \
|
|
||||||
--restart unless-stopped \
|
|
||||||
gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Troubleshooting
|
|
||||||
|
|
||||||
### Errore: "unauthorized: authentication required"
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Re-autentica con Token PAT
|
|
||||||
docker logout gitea.encke-hake.ts.net
|
|
||||||
docker login gitea.encke-hake.ts.net
|
|
||||||
# Username: Alby96
|
|
||||||
# Password: [TOKEN PAT]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Errore: "denied: requested access to the resource is denied"
|
|
||||||
|
|
||||||
**Causa:** Token PAT senza permessi corretti o scaduto
|
|
||||||
|
|
||||||
**Soluzione:**
|
|
||||||
1. Vai su: `https://gitea.encke-hake.ts.net/user/settings/applications`
|
|
||||||
2. Verifica che il token abbia: `read:packages` + `write:packages`
|
|
||||||
3. Se scaduto, genera nuovo token
|
|
||||||
|
|
||||||
### Container non parte: errore certificato HTTPS
|
|
||||||
|
|
||||||
**Sintomo:**
|
|
||||||
```
|
|
||||||
System.InvalidOperationException: Unable to configure HTTPS endpoint.
|
|
||||||
No server certificate was specified, and the default developer certificate
|
|
||||||
could not be found or is out of date.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Causa:** Kestrel cerca di abilitare HTTPS ma non trova certificati di sviluppo nel container.
|
|
||||||
|
|
||||||
**? RISOLTO:**
|
|
||||||
- HTTPS disabilitato di default in container (`Kestrel__EnableHttps=false`)
|
|
||||||
- Porta HTTP: `8080` (standard container)
|
|
||||||
- SSL gestito dal reverse proxy (nginx/traefik) in production
|
|
||||||
|
|
||||||
**Per abilitare HTTPS manualmente** (se hai un certificato):
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
-e Kestrel__EnableHttps=true \
|
|
||||||
-e Kestrel__Certificates__Default__Path=/path/to/cert.pfx \
|
|
||||||
-e Kestrel__Certificates__Default__Password=yourpassword \
|
|
||||||
-v /path/to/certs:/certs \
|
|
||||||
gitea.../autobidder:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Errore: "La compilazione non è riuscita" ma il push è riuscito
|
|
||||||
|
|
||||||
**Sintomo:**
|
|
||||||
Visual Studio mostra:
|
|
||||||
```
|
|
||||||
Errore MSB4057: la destinazione "ContainerBuild" non è presente nel progetto
|
|
||||||
```
|
|
||||||
|
|
||||||
Ma nel log vedi:
|
|
||||||
```
|
|
||||||
? Pushed: gitea.../autobidder:latest
|
|
||||||
? Pushed: gitea.../autobidder:1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
**Causa:** Il profilo stava usando `WebPublishMethod=Docker` che richiede Microsoft.Docker.Sdk non installato.
|
|
||||||
|
|
||||||
**? RISOLTO:** Il profilo è stato corretto per usare `WebPublishMethod=Custom` che non richiede SDK aggiuntivi.
|
|
||||||
|
|
||||||
### Verifica push su Gitea
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test manuale push
|
|
||||||
docker push gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
|
|
||||||
# Se fallisce, verifica autenticazione
|
|
||||||
docker logout gitea.encke-hake.ts.net
|
|
||||||
docker login gitea.encke-hake.ts.net
|
|
||||||
```
|
|
||||||
|
|
||||||
### Versione non cambia su Gitea
|
|
||||||
|
|
||||||
**Verifica:**
|
|
||||||
1. Hai modificato `<Version>` in `AutoBidder.csproj`?
|
|
||||||
2. Hai fatto Rebuild completo?
|
|
||||||
3. Visual Studio ha mostrato il nuovo numero versione nell'output?
|
|
||||||
|
|
||||||
**Soluzione:** Rebuild completo
|
|
||||||
```bash
|
|
||||||
# Da Visual Studio: Build ? Rebuild Solution
|
|
||||||
# Poi: Tasto destro ? Pubblica ? GiteaRegistry
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Riferimenti
|
|
||||||
|
|
||||||
- **Registry URL**: `https://gitea.encke-hake.ts.net`
|
|
||||||
- **Repository Codice**: `https://gitea.encke-hake.ts.net/Alby96/Mimante`
|
|
||||||
- **Packages Container**: `https://gitea.encke-hake.ts.net/Alby96/-/packages`
|
|
||||||
- **Package Autobidder**: `https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder`
|
|
||||||
- **Convenzione Gitea**: `{registro}/{owner}/{image}:{tag}` (3 livelli)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Riepilogo Comandi Rapidi
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Autenticazione (prima volta)
|
|
||||||
docker login gitea.encke-hake.ts.net
|
|
||||||
|
|
||||||
# 2. Pubblica da Visual Studio
|
|
||||||
# Tasto destro ? Pubblica ? GiteaRegistry
|
|
||||||
|
|
||||||
# 3. Pull versione specifica
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
|
|
||||||
|
|
||||||
# 4. Pull latest
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
|
|
||||||
# 5. Run container
|
|
||||||
docker run -d --name autobidder \
|
|
||||||
-p 5000:8080 \
|
|
||||||
-v /data:/app/Data \
|
|
||||||
gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**? CONFIGURAZIONE COMPLETATA!**
|
|
||||||
|
|
||||||
Ora hai un sistema di pubblicazione Docker con **versionamento automatico** completamente integrato! ??
|
|
||||||
|
|
||||||
## 3. Dove Trovare il Package su Gitea
|
|
||||||
|
|
||||||
**IL PACKAGE E' PUBBLICATO!** Cercalo in uno di questi percorsi:
|
|
||||||
|
|
||||||
### Percorso 1: Packages del Tuo Profilo (PRINCIPALE)
|
|
||||||
```
|
|
||||||
https://gitea.encke-hake.ts.net/Alby96/-/packages
|
|
||||||
```
|
|
||||||
Cerca un package di tipo **Container** con nome: `mimante/autobidder` oppure `mimante`
|
|
||||||
|
|
||||||
### Percorso 2: Explore Packages
|
|
||||||
```
|
|
||||||
https://gitea.encke-hake.ts.net/explore/packages
|
|
||||||
```
|
|
||||||
Filtra per tipo "Container" e cerca `mimante` o `autobidder`
|
|
||||||
|
|
||||||
### Percorso 3: Packages del Repository
|
|
||||||
```
|
|
||||||
https://gitea.encke-hake.ts.net/Alby96/Mimante/-/packages
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verifica Push Riuscito
|
|
||||||
|
|
||||||
Se hai eseguito il push e vedi nell'output:
|
|
||||||
```
|
|
||||||
latest: digest: sha256:cb7621ed1f22... size: 856
|
|
||||||
```
|
|
||||||
Significa che **il package E' STATO pubblicato correttamente!**
|
|
||||||
|
|
||||||
Per verificare:
|
|
||||||
```bash
|
|
||||||
docker push gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Usare l'Immagine Pubblicata
|
|
||||||
|
|
||||||
### Su Unraid
|
|
||||||
|
|
||||||
1. Vai su **Docker** tab
|
|
||||||
2. Click **Add Container**
|
|
||||||
3. **Repository**: `gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest`
|
|
||||||
4. **Port**: `5000` -> `8080` (container)
|
|
||||||
5. **Volume**: `/mnt/user/appdata/autobidder/data` -> `/app/Data`
|
|
||||||
6. **Volume**: `/mnt/user/appdata/autobidder/logs` -> `/app/logs`
|
|
||||||
7. **Environment**: `ASPNETCORE_ENVIRONMENT=Production`
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
autobidder:
|
|
||||||
image: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
container_name: autobidder
|
|
||||||
ports:
|
|
||||||
- "5000:8080"
|
|
||||||
volumes:
|
|
||||||
- ./data:/app/Data
|
|
||||||
- ./logs:/app/logs
|
|
||||||
environment:
|
|
||||||
- ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
restart: unless-stopped
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
|
|
||||||
|
|
||||||
docker run -d --name autobidder -p 5000:8080 -v /path/to/data:/app/Data -v /path/to/logs:/app/logs -e ASPNETCORE_ENVIRONMENT=Production gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Aggiornare la Versione
|
|
||||||
|
|
||||||
1. Apri `AutoBidder.csproj`
|
|
||||||
2. Modifica il tag `<Version>`:
|
|
||||||
```xml
|
|
||||||
<Version>1.0.1</Version>
|
|
||||||
```
|
|
||||||
3. Pubblica:
|
|
||||||
```bash
|
|
||||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Errore: "unauthorized: authentication required"
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker login gitea.encke-hake.ts.net
|
|
||||||
```
|
|
||||||
|
|
||||||
### Package non visibile su Gitea
|
|
||||||
|
|
||||||
**Il package c'e'!** Controlla in:
|
|
||||||
- `https://gitea.encke-hake.ts.net/Alby96/-/packages` (packages utente)
|
|
||||||
- `https://gitea.encke-hake.ts.net/explore/packages` (tutti)
|
|
||||||
|
|
||||||
Cerca per nome: `mimante`, `autobidder`, o `mimante/autobidder` (tipo: Container)
|
|
||||||
|
|
||||||
Se vedi `digest: sha256:...` nel push, il package E' pubblicato.
|
|
||||||
|
|
||||||
## Riferimenti
|
|
||||||
|
|
||||||
- **Registry**: `https://gitea.encke-hake.ts.net`
|
|
||||||
- **Repository**: `https://gitea.encke-hake.ts.net/Alby96/Mimante`
|
|
||||||
- **Packages**: `https://gitea.encke-hake.ts.net/Alby96/-/packages` ?
|
|
||||||
- **Package Diretto**: `https://gitea.encke-hake.ts.net/Alby96/-/packages/container/mimante%2Fautobidder/latest`
|
|
||||||
- **Immagine**: `gitea.encke-hake.ts.net/alby96/mimante/autobidder`
|
|
||||||
|
|
||||||
## Comandi Rapidi
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Login
|
|
||||||
docker login gitea.encke-hake.ts.net
|
|
||||||
|
|
||||||
# 2. Build e push
|
|
||||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
|
||||||
|
|
||||||
# 3. Pull
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
|
|
||||||
|
|
||||||
# 4. Run
|
|
||||||
docker run -d --name autobidder -p 5000:8080 -v /data:/app/Data gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Il package E' stato pubblicato!** Verifica su: `https://gitea.encke-hake.ts.net/Alby96/-/packages`
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
# ? FIX DEFINITIVO v1.1.2 - Porta Container
|
|
||||||
|
|
||||||
## ?? Problema Risolto
|
|
||||||
|
|
||||||
**Container ascoltava su porta 5000 invece di 8080**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Sintomi
|
|
||||||
|
|
||||||
```
|
|
||||||
docker logs AutoBidder:
|
|
||||||
Now listening on: http://[::]:5000 ?
|
|
||||||
|
|
||||||
Healthcheck:
|
|
||||||
curl: (7) Failed to connect to localhost port 8080 ?
|
|
||||||
|
|
||||||
Port mapping:
|
|
||||||
0.0.0.0:8889->8080/tcp ?
|
|
||||||
```
|
|
||||||
|
|
||||||
**Risultato:** Healthcheck unhealthy, applicazione non accessibile
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Root Cause
|
|
||||||
|
|
||||||
Dopo analisi approfondita dei log:
|
|
||||||
|
|
||||||
```
|
|
||||||
warn: Microsoft.AspNetCore.Server.Kestrel[0]
|
|
||||||
Overriding address(es) 'http://+:8080'.
|
|
||||||
Binding to endpoints defined via IConfiguration and/or UseKestrel() instead.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problema:** Una configurazione di default .NET sovra scriveva `ASPNETCORE_URLS`.
|
|
||||||
|
|
||||||
**Sospetti:**
|
|
||||||
- `launchSettings.json` con `applicationUrl: http://localhost:5000`
|
|
||||||
- Configurazioni Kestrel implicite
|
|
||||||
- Precedenza configurazione .NET vs env vars
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Soluzione Applicata
|
|
||||||
|
|
||||||
### 1. Forzato `UseUrls()` Esplicito
|
|
||||||
|
|
||||||
**Program.cs:**
|
|
||||||
```csharp
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
// FORCE ASPNETCORE_URLS to prevent any override
|
|
||||||
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_URLS")))
|
|
||||||
{
|
|
||||||
builder.WebHost.UseUrls("http://+:8080");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
builder.WebHost.UseUrls(Environment.GetEnvironmentVariable("ASPNETCORE_URLS")!);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefici:**
|
|
||||||
- ? Precedenza ASSOLUTA sulla porta
|
|
||||||
- ? Rispetta `ASPNETCORE_URLS` se definita
|
|
||||||
- ? Fallback sicuro a 8080
|
|
||||||
- ? Nessuna configurazione può sovrascrivere
|
|
||||||
|
|
||||||
### 2. Migliorato Healthcheck
|
|
||||||
|
|
||||||
**Dockerfile:**
|
|
||||||
```docker
|
|
||||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=90s --retries=5 \
|
|
||||||
CMD curl -f http://localhost:8080/ || exit 1
|
|
||||||
```
|
|
||||||
|
|
||||||
**Modifiche:**
|
|
||||||
- Timeout: 10s ? 30s
|
|
||||||
- Start period: 40s ? 90s
|
|
||||||
- Retries: 3 ? 5
|
|
||||||
|
|
||||||
**Motivo:** Blazor Server richiede più tempo per avviarsi completamente
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Come Aggiornare
|
|
||||||
|
|
||||||
### Opzione 1: Pull Nuova Immagine da Gitea
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Stop container vecchio
|
|
||||||
docker stop AutoBidder
|
|
||||||
docker rm AutoBidder
|
|
||||||
|
|
||||||
# Pull v1.1.2
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.1.2
|
|
||||||
|
|
||||||
# Avvia nuovo container
|
|
||||||
docker run -d \
|
|
||||||
--name AutoBidder \
|
|
||||||
-p 8889:8080 \
|
|
||||||
-v /mnt/user/appdata/autobidder/data:/app/Data \
|
|
||||||
gitea.encke-hake.ts.net/alby96/autobidder:1.1.2
|
|
||||||
```
|
|
||||||
|
|
||||||
### Opzione 2: Build Locale
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build nuova immagine
|
|
||||||
docker build -t autobidder:1.1.2 .
|
|
||||||
|
|
||||||
# Avvia container
|
|
||||||
docker run -d \
|
|
||||||
--name AutoBidder \
|
|
||||||
-p 8889:8080 \
|
|
||||||
-v /mnt/user/appdata/autobidder/data:/app/Data \
|
|
||||||
autobidder:1.1.2
|
|
||||||
```
|
|
||||||
|
|
||||||
### Opzione 3: Unraid
|
|
||||||
|
|
||||||
1. **Stop container**
|
|
||||||
2. **Edit template**
|
|
||||||
3. **Repository:** `gitea.encke-hake.ts.net/alby96/autobidder:1.1.2`
|
|
||||||
4. **Apply**
|
|
||||||
5. **Start container**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Verifica Fix
|
|
||||||
|
|
||||||
### 1. Controlla Log
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs AutoBidder | grep "listening"
|
|
||||||
|
|
||||||
# Output ATTESO:
|
|
||||||
# [Kestrel] Listening on: http://+:8080
|
|
||||||
# info: Now listening on: http://[::]:8080 ?
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Verifica Healthcheck
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Aspetta 90 secondi (start-period), poi:
|
|
||||||
docker inspect AutoBidder | grep -A 5 '"Status"'
|
|
||||||
|
|
||||||
# Output ATTESO:
|
|
||||||
# "Status": "healthy", ?
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Test Endpoint
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Dall'interno container
|
|
||||||
docker exec AutoBidder curl -f http://localhost:8080/
|
|
||||||
# Deve rispondere con HTML ?
|
|
||||||
|
|
||||||
# Dal browser
|
|
||||||
http://192.168.30.23:8889
|
|
||||||
# Homepage AutoBidder deve caricare ?
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Confronto Versioni
|
|
||||||
|
|
||||||
| Aspetto | v1.1.1 | v1.1.2 |
|
|
||||||
|---------|--------|--------|
|
|
||||||
| **Porta Ascolto** | ? 5000 | ? 8080 |
|
|
||||||
| **Healthcheck** | ? Unhealthy | ? Healthy |
|
|
||||||
| **Accessibilità** | ? Connection refused | ? Funzionante |
|
|
||||||
| **UseUrls() Forzato** | ? No | ? Sì |
|
|
||||||
| **Timeout Healthcheck** | 10s | 30s |
|
|
||||||
| **Start Period** | 40s | 90s |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Lezioni Apprese
|
|
||||||
|
|
||||||
### 1. ASPNETCORE_URLS Non Sempre Funziona
|
|
||||||
|
|
||||||
**Problema:** Variabile env può essere sovrascritta da:
|
|
||||||
- `launchSettings.json`
|
|
||||||
- Configurazioni IConfiguration
|
|
||||||
- Default Kestrel
|
|
||||||
|
|
||||||
**Soluzione:** Usare `UseUrls()` esplicito per precedenza assoluta
|
|
||||||
|
|
||||||
### 2. Healthcheck Deve Considerare App Type
|
|
||||||
|
|
||||||
**Blazor Server:**
|
|
||||||
- Richiede più tempo per avviarsi
|
|
||||||
- SignalR deve inizializzare
|
|
||||||
- Timeout default troppo brevi
|
|
||||||
|
|
||||||
**Best Practice:**
|
|
||||||
- Start period: almeno 60-90s
|
|
||||||
- Timeout: 30s
|
|
||||||
- Retries: 5+
|
|
||||||
|
|
||||||
### 3. Verifica Sempre i Log
|
|
||||||
|
|
||||||
**Comando essenziale:**
|
|
||||||
```bash
|
|
||||||
docker logs <container> | grep "listening"
|
|
||||||
```
|
|
||||||
|
|
||||||
Mostra la porta EFFETTIVA, non quella configurata!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Modificati
|
|
||||||
|
|
||||||
| File | Modifica | Motivo |
|
|
||||||
|------|----------|--------|
|
|
||||||
| **Program.cs** | Aggiunto `UseUrls()` forzato | Garantire porta corretta |
|
|
||||||
| **Dockerfile** | Healthcheck timeout/retries aumentati | Blazor Server startup |
|
|
||||||
| **AutoBidder.csproj** | Versione `1.1.2` | Incremento PATCH |
|
|
||||||
| **CHANGELOG.md** | Entry v1.1.2 | Documentazione fix |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Stato Finale
|
|
||||||
|
|
||||||
```
|
|
||||||
? Container ascolta su porta 8080
|
|
||||||
? Healthcheck passa (healthy)
|
|
||||||
? Applicazione accessibile da browser
|
|
||||||
? Port mapping corretto (8889:8080)
|
|
||||||
? Log mostra porta corretta
|
|
||||||
? Fix testato e verificato
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Prossimi Passi
|
|
||||||
|
|
||||||
### 1. Pubblica su Gitea
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Da Visual Studio
|
|
||||||
# Tasto destro ? Pubblica ? GiteaRegistry
|
|
||||||
|
|
||||||
# Oppure CLI
|
|
||||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Deploy su Unraid
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Aggiorna repository a:
|
|
||||||
gitea.encke-hake.ts.net/alby96/autobidder:1.1.2
|
|
||||||
|
|
||||||
# Restart container
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Verifica Finale
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Browser
|
|
||||||
http://192.168.30.23:8889
|
|
||||||
|
|
||||||
# Dovrebbe mostrare homepage AutoBidder ?
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**? v1.1.2 - FIX DEFINITIVO PORTA CONTAINER!**
|
|
||||||
|
|
||||||
Ora il container funziona correttamente! ??
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
# ? FIX APPLICATI - Errore NavigationException + Emoji Login
|
|
||||||
|
|
||||||
## ?? Analisi Errore nei Log
|
|
||||||
|
|
||||||
### Errore Rilevato
|
|
||||||
|
|
||||||
```
|
|
||||||
Eccezione generata: 'Microsoft.AspNetCore.Components.NavigationException'
|
|
||||||
in Microsoft.AspNetCore.Components.Server.dll
|
|
||||||
Eccezione di tipo 'Microsoft.AspNetCore.Components.NavigationException'
|
|
||||||
in Microsoft.AspNetCore.Components.Server.dll non gestita nel codice utente
|
|
||||||
```
|
|
||||||
|
|
||||||
### ? Spiegazione
|
|
||||||
|
|
||||||
**Questo NON è un errore da correggere!**
|
|
||||||
|
|
||||||
L'eccezione `NavigationException` è il comportamento **normale** e **previsto** quando si usa:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
Navigation.NavigateTo("/login", forceLoad: true);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Come funziona:**
|
|
||||||
|
|
||||||
1. `forceLoad: true` forza un refresh completo della pagina
|
|
||||||
2. Blazor Server lancia internamente una `NavigationException`
|
|
||||||
3. Il framework la gestisce correttamente
|
|
||||||
4. Il redirect viene eseguito con successo
|
|
||||||
5. L'applicazione continua a funzionare normalmente
|
|
||||||
|
|
||||||
**Evidenza dal log:**
|
|
||||||
```
|
|
||||||
Microsoft.Hosting.Lifetime: Information: Now listening on: http://localhost:5000
|
|
||||||
Microsoft.Hosting.Lifetime: Information: Application started. Press Ctrl+C to shut down.
|
|
||||||
```
|
|
||||||
|
|
||||||
? L'applicazione si è avviata correttamente
|
|
||||||
? Il redirect funziona
|
|
||||||
? Nessun crash o malfunzionamento
|
|
||||||
|
|
||||||
### ?? Riferimento Microsoft
|
|
||||||
|
|
||||||
Documentazione ufficiale:
|
|
||||||
> "NavigationException is thrown when NavigateTo is called with forceLoad: true.
|
|
||||||
> This is expected behavior and should not be caught or handled."
|
|
||||||
|
|
||||||
[ASP.NET Core Blazor Routing - NavigationException](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? FIX: Rimozione Emoji dalla Pagina Login
|
|
||||||
|
|
||||||
### Problema
|
|
||||||
|
|
||||||
Caratteri `??` visualizzati al posto di emoji nella pagina di login.
|
|
||||||
|
|
||||||
**Causa:** Font Windows che non supportano emoji Unicode moderni.
|
|
||||||
|
|
||||||
### Emoji Rimossi
|
|
||||||
|
|
||||||
**Prima:**
|
|
||||||
```razor
|
|
||||||
<h2>?? AutoBidder</h2>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Dopo:**
|
|
||||||
```razor
|
|
||||||
<h2>AutoBidder</h2>
|
|
||||||
```
|
|
||||||
|
|
||||||
### File Modificato
|
|
||||||
|
|
||||||
- `Pages/Login.razor` - Rimosso emoji dal titolo
|
|
||||||
|
|
||||||
**Risultato:** Titolo pulito e leggibile su tutti i sistemi Windows.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Credenziali di Default
|
|
||||||
|
|
||||||
### Configurazione Attuale
|
|
||||||
|
|
||||||
**Username di default:**
|
|
||||||
```docker
|
|
||||||
# Dockerfile
|
|
||||||
ENV ADMIN_USERNAME=admin
|
|
||||||
```
|
|
||||||
|
|
||||||
**Password di default:**
|
|
||||||
```csharp
|
|
||||||
// Program.cs (già implementato)
|
|
||||||
var adminPassword = Environment.GetEnvironmentVariable("ADMIN_PASSWORD");
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(adminPassword))
|
|
||||||
{
|
|
||||||
Console.WriteLine("[Identity] WARNING: ADMIN_PASSWORD not set! Using default password.");
|
|
||||||
Console.WriteLine("[Identity] CHANGE IT IMMEDIATELY after first login!");
|
|
||||||
adminPassword = "Admin@Password123!"; // Password temporanea FORTE
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Credenziali Preimpostate
|
|
||||||
|
|
||||||
| Campo | Valore Default | Configurabile |
|
|
||||||
|-------|---------------|---------------|
|
|
||||||
| **Username** | `admin` | ? Sì (via `ADMIN_USERNAME`) |
|
|
||||||
| **Password** | `Admin@Password123!` | ? Sì (via `ADMIN_PASSWORD`) |
|
|
||||||
|
|
||||||
### Come Funziona
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Container avviato
|
|
||||||
?
|
|
||||||
2. Program.cs legge ADMIN_PASSWORD
|
|
||||||
?
|
|
||||||
3. Se ADMIN_PASSWORD vuota:
|
|
||||||
- Usa password default: Admin@Password123!
|
|
||||||
- WARNING nei log ??
|
|
||||||
?
|
|
||||||
4. Se ADMIN_PASSWORD configurata:
|
|
||||||
- Usa quella password
|
|
||||||
- Nessun warning ?
|
|
||||||
```
|
|
||||||
|
|
||||||
### Primo Login
|
|
||||||
|
|
||||||
**Con credenziali di default:**
|
|
||||||
```
|
|
||||||
Username: admin
|
|
||||||
Password: Admin@Password123!
|
|
||||||
```
|
|
||||||
|
|
||||||
**?? Container mostrerà:**
|
|
||||||
```
|
|
||||||
[Identity] WARNING: ADMIN_PASSWORD not set! Using default password.
|
|
||||||
[Identity] CHANGE IT IMMEDIATELY after first login!
|
|
||||||
[Identity] Admin user created: admin
|
|
||||||
[Identity] ?? REMEMBER TO CHANGE THE DEFAULT PASSWORD!
|
|
||||||
```
|
|
||||||
|
|
||||||
### Visualizzazione Credenziali nella Pagina Login
|
|
||||||
|
|
||||||
**NUOVO**: Se `ADMIN_PASSWORD` non è configurata, la pagina di login mostra le credenziali di default:
|
|
||||||
|
|
||||||
```razor
|
|
||||||
@if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ADMIN_PASSWORD")))
|
|
||||||
{
|
|
||||||
<div class="mt-3 p-3 bg-warning bg-opacity-10 border border-warning rounded">
|
|
||||||
<p class="mb-1 small"><strong>Credenziali di default:</strong></p>
|
|
||||||
<p class="mb-0 small">Username: <code>admin</code></p>
|
|
||||||
<p class="mb-0 small">Password: <code>Admin@Password123!</code></p>
|
|
||||||
<p class="mb-0 small text-danger mt-2"><strong>CAMBIARE IMMEDIATAMENTE!</strong></p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Vantaggi:**
|
|
||||||
- ? Utente sa subito quali credenziali usare
|
|
||||||
- ? Warning visibile per cambio password
|
|
||||||
- ? Box appare SOLO se password non configurata
|
|
||||||
- ? Produzione con ADMIN_PASSWORD configurata: box NON appare
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Test Completo
|
|
||||||
|
|
||||||
### Test 1: Avvio con Password di Default
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# NON configurare ADMIN_PASSWORD
|
|
||||||
docker run -d -p 8889:8080 autobidder:1.2.0
|
|
||||||
|
|
||||||
# Log attesi:
|
|
||||||
[Identity] WARNING: ADMIN_PASSWORD not set! Using default password.
|
|
||||||
[Identity] Admin user created: admin
|
|
||||||
[Identity] ?? REMEMBER TO CHANGE THE DEFAULT PASSWORD!
|
|
||||||
|
|
||||||
# Pagina login:
|
|
||||||
- Titolo: "AutoBidder" (senza emoji ?)
|
|
||||||
- Box giallo con credenziali: VISIBILE ?
|
|
||||||
- Username: admin
|
|
||||||
- Password: Admin@Password123!
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 2: Avvio con Password Configurata
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
-p 8889:8080 \
|
|
||||||
-e ADMIN_PASSWORD="MyS3cur3P@ss!2024" \
|
|
||||||
autobidder:1.2.0
|
|
||||||
|
|
||||||
# Log attesi:
|
|
||||||
[Identity] Admin user created: admin
|
|
||||||
(NESSUN warning)
|
|
||||||
|
|
||||||
# Pagina login:
|
|
||||||
- Titolo: "AutoBidder" (senza emoji ?)
|
|
||||||
- Box giallo credenziali: NON VISIBILE ?
|
|
||||||
- Username: admin
|
|
||||||
- Password: MyS3cur3P@ss!2024
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 3: Redirect Login Funziona
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Browser: http://localhost:8889
|
|
||||||
2. REDIRECT AUTOMATICO ? /login ?
|
|
||||||
3. Nessun errore visibile ?
|
|
||||||
4. Log: NavigationException (normale) ?
|
|
||||||
5. Pagina login carica ?
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Checklist Correzioni
|
|
||||||
|
|
||||||
- [x] **Analizzato errore NavigationException** ? Comportamento normale ?
|
|
||||||
- [x] **Rimosso emoji da Login.razor** ? Titolo pulito ?
|
|
||||||
- [x] **Verificato credenziali di default** ? Già implementate ?
|
|
||||||
- [x] **Aggiunto box credenziali in pagina login** ? Per sviluppo/test ?
|
|
||||||
- [x] **Dockerfile con ADMIN_USERNAME=admin** ? Default corretto ?
|
|
||||||
- [x] **Program.cs con fallback password** ? Admin@Password123! ?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Risultato Finale
|
|
||||||
|
|
||||||
### Comportamento Corretto
|
|
||||||
|
|
||||||
```
|
|
||||||
Primo avvio (senza ADMIN_PASSWORD configurata):
|
|
||||||
|
|
||||||
1. Container parte ?
|
|
||||||
2. Log WARNING password default ?
|
|
||||||
3. Utente admin creato con password temporanea ?
|
|
||||||
4. Browser ? redirect a /login ?
|
|
||||||
5. Pagina login mostra box giallo con credenziali ?
|
|
||||||
6. Login con admin / Admin@Password123! ?
|
|
||||||
7. Accesso homepage AutoBidder ?
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sicurezza Mantenuta
|
|
||||||
|
|
||||||
- ? Password default FORTE (12+ caratteri, simboli, numeri)
|
|
||||||
- ? Warning visibili nei log se usa password default
|
|
||||||
- ? Box credenziali appare SOLO in sviluppo (ADMIN_PASSWORD non configurata)
|
|
||||||
- ? Produzione con ADMIN_PASSWORD ? nessun warning, nessun box
|
|
||||||
|
|
||||||
### User Experience Migliorata
|
|
||||||
|
|
||||||
- ? Emoji rimossi ? titolo leggibile su tutti i sistemi
|
|
||||||
- ? Credenziali default visibili ? primo accesso facile
|
|
||||||
- ? Warning chiari ? sicurezza rafforzata
|
|
||||||
- ? Nessun errore visibile ? esperienza pulita
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Modificati
|
|
||||||
|
|
||||||
| File | Modifica | Motivo |
|
|
||||||
|------|----------|--------|
|
|
||||||
| `Pages/Login.razor` | Rimosso emoji `??` | Fix caratteri ?? su Windows |
|
|
||||||
| `Pages/Login.razor` | Aggiunto box credenziali default | UX migliorata per sviluppo |
|
|
||||||
|
|
||||||
**Nessuna modifica a:**
|
|
||||||
- `Program.cs` - Logica password default già presente ?
|
|
||||||
- `Dockerfile` - ADMIN_USERNAME già configurato ?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Prossimi Passi
|
|
||||||
|
|
||||||
### Per Sviluppatore
|
|
||||||
|
|
||||||
1. ? Nessuna modifica necessaria
|
|
||||||
2. ? Funziona già correttamente
|
|
||||||
3. ? Testare login con credenziali default
|
|
||||||
|
|
||||||
### Per Utente Finale
|
|
||||||
|
|
||||||
1. **Primo deploy:**
|
|
||||||
```bash
|
|
||||||
docker run -d -p 8889:8080 autobidder:1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Login con credenziali default:**
|
|
||||||
- Username: `admin`
|
|
||||||
- Password: `Admin@Password123!`
|
|
||||||
|
|
||||||
3. **Configurazione produzione:**
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
-p 8889:8080 \
|
|
||||||
-e ADMIN_PASSWORD="MiaPasswordSicura!2024" \
|
|
||||||
autobidder:1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**? TUTTO RISOLTO!**
|
|
||||||
|
|
||||||
- ? Errore NavigationException: comportamento normale
|
|
||||||
- ? Emoji rimossi: pagina login pulita
|
|
||||||
- ? Credenziali default: configurate e documentate
|
|
||||||
- ? Box informativo: visibile solo quando necessario
|
|
||||||
|
|
||||||
**?? Pronto per il deploy!**
|
|
||||||
@@ -1,402 +0,0 @@
|
|||||||
# ? FIX: Errore SectionRegistry - Layout Duplicato Risolto
|
|
||||||
|
|
||||||
## ?? Errore Identificato
|
|
||||||
|
|
||||||
```
|
|
||||||
System.InvalidOperationException: There is already a subscriber to the content
|
|
||||||
with the given section ID 'System.Object'.
|
|
||||||
at Microsoft.AspNetCore.Components.Sections.SectionRegistry.Subscribe
|
|
||||||
```
|
|
||||||
|
|
||||||
**Causa:** `LoginLayout.razor` conteneva un HTML completo con `<HeadOutlet />`, creando un duplicato con quello già presente in `_Host.cshtml`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ??? Architettura Blazor Server
|
|
||||||
|
|
||||||
### Come Funziona il Rendering
|
|
||||||
|
|
||||||
```
|
|
||||||
_Host.cshtml (HTML esterno)
|
|
||||||
?
|
|
||||||
<component type="typeof(App)" />
|
|
||||||
?
|
|
||||||
App.razor (Router)
|
|
||||||
?
|
|
||||||
Layout (MainLayout o LoginLayout)
|
|
||||||
?
|
|
||||||
Page (Index, Login, etc.)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Regola importante:** Solo `_Host.cshtml` deve contenere:
|
|
||||||
- `<!DOCTYPE html>`
|
|
||||||
- `<html>`, `<head>`, `<body>`
|
|
||||||
- `<HeadOutlet />`
|
|
||||||
|
|
||||||
I **Layout** (`.razor`) devono contenere SOLO:
|
|
||||||
- `@inherits LayoutComponentBase`
|
|
||||||
- `@Body` per il contenuto
|
|
||||||
- CSS/JS inline se necessario
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Soluzione Applicata
|
|
||||||
|
|
||||||
### Prima (ERRATO - causava duplicazione)
|
|
||||||
|
|
||||||
```razor
|
|
||||||
@inherits LayoutComponentBase
|
|
||||||
|
|
||||||
<!DOCTYPE html> ? ? DUPLICATO (già in _Host.cshtml)
|
|
||||||
<html lang="it"> ? ? DUPLICATO
|
|
||||||
<head> ? ? DUPLICATO
|
|
||||||
<HeadOutlet /> ? ? DUPLICATO (già in _Host.cshtml)
|
|
||||||
</head>
|
|
||||||
<body> ? ? DUPLICATO
|
|
||||||
@Body
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problema:** `_Host.cshtml` ha già `<HeadOutlet />`, creando quindi DUE outlet con lo stesso ID.
|
|
||||||
|
|
||||||
### Dopo (CORRETTO - minimal layout)
|
|
||||||
|
|
||||||
```razor
|
|
||||||
@inherits LayoutComponentBase
|
|
||||||
|
|
||||||
<div class="login-page">
|
|
||||||
@Body
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.login-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-page + .sidebar,
|
|
||||||
.login-page .sidebar {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Vantaggi:**
|
|
||||||
- ? Nessuna duplicazione HTML
|
|
||||||
- ? Nessun `<HeadOutlet />` duplicato
|
|
||||||
- ? CSS inline per nascondere sidebar
|
|
||||||
- ? Fullscreen layout per login
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Come Funziona Ora
|
|
||||||
|
|
||||||
### Rendering Pagina Login
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Browser richiede: http://localhost:5000
|
|
||||||
?
|
|
||||||
2. _Host.cshtml renderizza:
|
|
||||||
- <html>, <head>, <body>
|
|
||||||
- <HeadOutlet /> (UNICO)
|
|
||||||
- <component type="typeof(App)" />
|
|
||||||
?
|
|
||||||
3. App.razor (Router):
|
|
||||||
- Controlla autenticazione
|
|
||||||
- Utente non autenticato ? <RedirectToLogin />
|
|
||||||
?
|
|
||||||
4. RedirectToLogin:
|
|
||||||
- Spinner "Reindirizzamento..."
|
|
||||||
- Navigation.NavigateTo("/login")
|
|
||||||
?
|
|
||||||
5. Login.razor:
|
|
||||||
- @layout LoginLayout
|
|
||||||
- LoginLayout.razor renderizza:
|
|
||||||
<div class="login-page">
|
|
||||||
@Body (Login.razor)
|
|
||||||
</div>
|
|
||||||
?
|
|
||||||
6. ? Pagina login PULITA:
|
|
||||||
- Nessuna sidebar
|
|
||||||
- Solo form login
|
|
||||||
- Nessun errore SectionRegistry
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rendering Dopo Login
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Login riuscito
|
|
||||||
?
|
|
||||||
2. Navigation.NavigateTo("/")
|
|
||||||
?
|
|
||||||
3. App.razor ? AuthorizeRouteView
|
|
||||||
- Utente autenticato ?
|
|
||||||
?
|
|
||||||
4. Index.razor:
|
|
||||||
- @attribute [Authorize]
|
|
||||||
- Usa MainLayout (default)
|
|
||||||
- MainLayout ha sidebar/menu
|
|
||||||
?
|
|
||||||
5. ? Dashboard completa:
|
|
||||||
- Sidebar visibile
|
|
||||||
- Menu funzionante
|
|
||||||
- UI completa
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Confronto Layout
|
|
||||||
|
|
||||||
### MainLayout.razor (App Principale)
|
|
||||||
|
|
||||||
```razor
|
|
||||||
@inherits LayoutComponentBase
|
|
||||||
|
|
||||||
<div class="page">
|
|
||||||
<div class="sidebar">
|
|
||||||
<NavMenu />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<div class="top-row px-4">
|
|
||||||
<!-- Header -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<article class="content px-4">
|
|
||||||
@Body
|
|
||||||
</article>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Usato da:**
|
|
||||||
- Index.razor
|
|
||||||
- FreeBids.razor
|
|
||||||
- Statistics.razor
|
|
||||||
- Settings.razor
|
|
||||||
- Health.razor
|
|
||||||
|
|
||||||
### LoginLayout.razor (Pagine Auth)
|
|
||||||
|
|
||||||
```razor
|
|
||||||
@inherits LayoutComponentBase
|
|
||||||
|
|
||||||
<div class="login-page">
|
|
||||||
@Body
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.login-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Usato da:**
|
|
||||||
- Login.razor
|
|
||||||
- Logout.razor
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Test Completo
|
|
||||||
|
|
||||||
### Test 1: Primo Avvio (Login)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. dotnet run
|
|
||||||
2. Browser: http://localhost:5000
|
|
||||||
3. ? Nessun errore SectionRegistry
|
|
||||||
4. ? Spinner "Reindirizzamento..." appare
|
|
||||||
5. ? Redirect a /login
|
|
||||||
6. ? Pagina login pulita (nessuna sidebar)
|
|
||||||
7. ? Form login funzionante
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 2: Login Riuscito
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Username: admin
|
|
||||||
2. Password: Admin@Password123!
|
|
||||||
3. Click "Accedi"
|
|
||||||
4. ? Redirect a homepage
|
|
||||||
5. ? Sidebar APPARE
|
|
||||||
6. ? Menu funzionante
|
|
||||||
7. ? Dashboard completa
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 3: Logout
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Click "Logout" in sidebar
|
|
||||||
2. ? Redirect a /logout
|
|
||||||
3. ? LoginLayout usato (nessuna sidebar)
|
|
||||||
4. ? Spinner "Disconnessione..."
|
|
||||||
5. ? Redirect a /login
|
|
||||||
6. ? Pagina login pulita
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 4: Accesso Diretto Pagina Protetta
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Logout
|
|
||||||
2. Browser: http://localhost:5000/settings
|
|
||||||
3. ? Spinner "Reindirizzamento..."
|
|
||||||
4. ? Redirect a /login
|
|
||||||
5. ? LoginLayout usato (nessuna sidebar)
|
|
||||||
6. Login ? redirect a /settings
|
|
||||||
7. ? MainLayout usato (sidebar visibile)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Checklist Correzioni
|
|
||||||
|
|
||||||
- [x] **LoginLayout.razor corretto** - Rimossi tag HTML duplicati
|
|
||||||
- [x] **HeadOutlet unico** - Solo in `_Host.cshtml`
|
|
||||||
- [x] **Layout minimal** - Solo `@Body` e CSS inline
|
|
||||||
- [x] **Build riuscita** - Nessun errore compilazione
|
|
||||||
- [x] **Errore SectionRegistry risolto** - Nessuna duplicazione
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Modificati
|
|
||||||
|
|
||||||
| File | Modifica | Motivo |
|
|
||||||
|------|----------|--------|
|
|
||||||
| `Shared/LoginLayout.razor` | Rimosso HTML completo | Evita duplicazione `<HeadOutlet />` |
|
|
||||||
|
|
||||||
**File NON modificati:**
|
|
||||||
- `Pages/_Host.cshtml` - Già corretto ?
|
|
||||||
- `App.razor` - Già corretto ?
|
|
||||||
- `Pages/Login.razor` - Già usa `@layout LoginLayout` ?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Best Practices Blazor Server
|
|
||||||
|
|
||||||
### ? DO
|
|
||||||
|
|
||||||
```razor
|
|
||||||
<!-- Layout.razor -->
|
|
||||||
@inherits LayoutComponentBase
|
|
||||||
|
|
||||||
<div class="my-layout">
|
|
||||||
@Body
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Stili inline OK */
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
|
|
||||||
### ? DON'T
|
|
||||||
|
|
||||||
```razor
|
|
||||||
<!-- Layout.razor - ERRATO! -->
|
|
||||||
@inherits LayoutComponentBase
|
|
||||||
|
|
||||||
<!DOCTYPE html> ? ? NO! Già in _Host.cshtml
|
|
||||||
<html> ? ? NO!
|
|
||||||
<head> ? ? NO!
|
|
||||||
<HeadOutlet /> ? ? NO! Causa duplicazione
|
|
||||||
</head>
|
|
||||||
<body> ? ? NO!
|
|
||||||
@Body
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Struttura Corretta
|
|
||||||
|
|
||||||
```
|
|
||||||
_Host.cshtml:
|
|
||||||
- <!DOCTYPE html>
|
|
||||||
- <html>, <head>, <body>
|
|
||||||
- <HeadOutlet /> (UNICO)
|
|
||||||
- <component type="typeof(App)" />
|
|
||||||
|
|
||||||
App.razor:
|
|
||||||
- <Router>
|
|
||||||
- <AuthorizeRouteView>
|
|
||||||
- Layout routing
|
|
||||||
|
|
||||||
Layout.razor:
|
|
||||||
- @inherits LayoutComponentBase
|
|
||||||
- @Body
|
|
||||||
- CSS/JS inline opzionale
|
|
||||||
|
|
||||||
Page.razor:
|
|
||||||
- @page "/route"
|
|
||||||
- @layout LayoutName (opzionale)
|
|
||||||
- Contenuto pagina
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Troubleshooting
|
|
||||||
|
|
||||||
### Errore: "There is already a subscriber to the content with the given section ID"
|
|
||||||
|
|
||||||
**Causa:** Doppio `<HeadOutlet />` o `<SectionOutlet>`
|
|
||||||
|
|
||||||
**Verifica:**
|
|
||||||
1. `_Host.cshtml` deve avere UN SOLO `<HeadOutlet />`
|
|
||||||
2. Layout (`.razor`) NON devono avere `<HeadOutlet />`
|
|
||||||
3. Layout NON devono avere tag `<html>`, `<head>`, `<body>`
|
|
||||||
|
|
||||||
**Soluzione:**
|
|
||||||
- Rimuovi tag HTML duplicati dai layout
|
|
||||||
- Lascia solo `@Body` e CSS inline nei layout
|
|
||||||
|
|
||||||
### Errore: "Cannot find component 'HeadOutlet'"
|
|
||||||
|
|
||||||
**Causa:** Manca import namespace
|
|
||||||
|
|
||||||
**Soluzione:**
|
|
||||||
```razor
|
|
||||||
@using Microsoft.AspNetCore.Components.Web
|
|
||||||
```
|
|
||||||
|
|
||||||
Oppure aggiungi in `_Imports.razor`:
|
|
||||||
```razor
|
|
||||||
@using Microsoft.AspNetCore.Components.Web
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? RISOLTO!
|
|
||||||
|
|
||||||
- ? Errore `SectionRegistry` eliminato
|
|
||||||
- ? Layout corretto e minimal
|
|
||||||
- ? Nessuna duplicazione HTML
|
|
||||||
- ? Sidebar nascosta in pagina login
|
|
||||||
- ? Build riuscita
|
|
||||||
- ? Pronto per test locale
|
|
||||||
|
|
||||||
**?? L'applicazione ora funziona correttamente!**
|
|
||||||
|
|
||||||
### Test Finale
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Build
|
|
||||||
dotnet build
|
|
||||||
|
|
||||||
# 2. Run
|
|
||||||
dotnet run
|
|
||||||
|
|
||||||
# 3. Browser
|
|
||||||
http://localhost:5000
|
|
||||||
|
|
||||||
# Risultato atteso:
|
|
||||||
? Pagina login pulita (nessuna sidebar)
|
|
||||||
? Nessun errore SectionRegistry
|
|
||||||
? Login funzionante
|
|
||||||
? Dopo login: sidebar appare
|
|
||||||
? UX professionale
|
|
||||||
```
|
|
||||||
|
|
||||||
**?? Pronto per il deploy production!**
|
|
||||||
@@ -1,386 +0,0 @@
|
|||||||
# ? FIX: Errore "Headers are read-only" al Login
|
|
||||||
|
|
||||||
## ?? Errore Originale
|
|
||||||
|
|
||||||
```
|
|
||||||
Errore durante il login: Headers are read-only, response has already started.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Sintomo:** Dopo aver inserito username/password e cliccato "Accedi", l'errore appare e il login non funziona.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Causa del Problema
|
|
||||||
|
|
||||||
**Codice problematico:**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Login.razor - HandleLogin()
|
|
||||||
if (result.Succeeded)
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo(ReturnUrl ?? "/", forceLoad: true); // ? ERRORE!
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Perché l'errore?**
|
|
||||||
|
|
||||||
In Blazor Server, quando un componente è **interattivo** (già renderizzato e connesso via SignalR):
|
|
||||||
|
|
||||||
1. Utente clicca "Accedi"
|
|
||||||
2. `HandleLogin()` viene eseguito
|
|
||||||
3. `SignInManager.PasswordSignInAsync()` crea cookie di autenticazione
|
|
||||||
4. Componente è ancora renderizzato e interattivo
|
|
||||||
5. `Navigation.NavigateTo(..., forceLoad: true)` tenta di:
|
|
||||||
- Modificare header HTTP (per refresh completo)
|
|
||||||
- **MA** la risposta HTTP è già stata inviata al client
|
|
||||||
6. ? **Exception:** "Headers are read-only, response has already started"
|
|
||||||
|
|
||||||
### Differenza forceLoad
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// forceLoad: true
|
|
||||||
// - Fa un refresh completo della pagina (come F5)
|
|
||||||
// - Tenta di modificare header HTTP
|
|
||||||
// - ? ERRORE se componente già renderizzato
|
|
||||||
|
|
||||||
// forceLoad: false (default)
|
|
||||||
// - Usa navigazione Blazor Server (SignalR)
|
|
||||||
// - Non modifica header HTTP
|
|
||||||
// - ? FUNZIONA sempre
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Soluzione Applicata
|
|
||||||
|
|
||||||
### Fix 1: HandleLogin (dopo login riuscito)
|
|
||||||
|
|
||||||
**Prima (ERRORE):**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
if (result.Succeeded)
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo(ReturnUrl ?? "/", forceLoad: true); // ?
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Dopo (CORRETTO):**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
if (result.Succeeded)
|
|
||||||
{
|
|
||||||
// Login riuscito - redirect senza forceLoad
|
|
||||||
Navigation.NavigateTo(ReturnUrl ?? "/"); // ?
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fix 2: OnInitializedAsync (se già autenticato)
|
|
||||||
|
|
||||||
**Prima:**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
|
||||||
if (authState.User.Identity?.IsAuthenticated == true)
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo(ReturnUrl ?? "/"); // Già corretto
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nota:** Questo era già corretto (nessun `forceLoad`), ma ho aggiunto commento per chiarezza.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Come Funziona Ora
|
|
||||||
|
|
||||||
### Flusso Login Corretto
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Utente inserisce username/password
|
|
||||||
?
|
|
||||||
2. Click "Accedi"
|
|
||||||
?
|
|
||||||
3. HandleLogin() eseguito
|
|
||||||
?
|
|
||||||
4. SignInManager.PasswordSignInAsync()
|
|
||||||
?
|
|
||||||
5. Cookie di autenticazione creato ?
|
|
||||||
?
|
|
||||||
6. Navigation.NavigateTo("/") (SENZA forceLoad)
|
|
||||||
?
|
|
||||||
7. Blazor Server gestisce navigazione via SignalR
|
|
||||||
?
|
|
||||||
8. ? Redirect a homepage
|
|
||||||
?
|
|
||||||
9. AuthorizeRouteView controlla autenticazione
|
|
||||||
?
|
|
||||||
10. ? Utente autenticato - homepage carica
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nessun refresh completo necessario!** Blazor Server gestisce tutto via SignalR.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Test della Soluzione
|
|
||||||
|
|
||||||
### Test 1: Login Normale
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Browser: http://localhost:5000
|
|
||||||
2. Redirect a /login
|
|
||||||
3. Username: admin
|
|
||||||
4. Password: Admin@Password123!
|
|
||||||
5. Click "Accedi"
|
|
||||||
6. ? Nessun errore
|
|
||||||
7. ? Redirect a homepage
|
|
||||||
8. ? Sidebar e menu visibili
|
|
||||||
9. ? Autenticato correttamente
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 2: Login con ReturnUrl
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Browser: http://localhost:5000/settings (non autenticato)
|
|
||||||
2. Redirect a /login?returnUrl=%2Fsettings
|
|
||||||
3. Inserisci credenziali
|
|
||||||
4. Click "Accedi"
|
|
||||||
5. ? Nessun errore
|
|
||||||
6. ? Redirect automatico a /settings
|
|
||||||
7. ? Pagina Settings carica
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 3: Password Errata
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Username: admin
|
|
||||||
2. Password: wrong_password
|
|
||||||
3. Click "Accedi"
|
|
||||||
4. ? Messaggio: "Username o password non validi."
|
|
||||||
5. ? Nessun redirect
|
|
||||||
6. ? Rimane sulla pagina login
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 4: Account Bloccato
|
|
||||||
|
|
||||||
```
|
|
||||||
1. 5 tentativi con password errata
|
|
||||||
2. ? Messaggio: "Account temporaneamente bloccato..."
|
|
||||||
3. ? Nessun errore "Headers are read-only"
|
|
||||||
4. Aspetta 5 minuti
|
|
||||||
5. Login con password corretta
|
|
||||||
6. ? Funziona
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Differenza forceLoad
|
|
||||||
|
|
||||||
| Aspetto | `forceLoad: false` (default) | `forceLoad: true` |
|
|
||||||
|---------|------------------------------|-------------------|
|
|
||||||
| **Metodo** | Navigazione SignalR | Refresh browser |
|
|
||||||
| **Header HTTP** | Non modificati | Modificati |
|
|
||||||
| **Stato componente** | Preservato | Perso |
|
|
||||||
| **Cookie** | Già inviati | Inviati di nuovo |
|
|
||||||
| **Errore "Headers read-only"** | ? Mai | ? Possibile |
|
|
||||||
| **Performance** | ? Veloce | ?? Lento |
|
|
||||||
| **Quando usare** | ? Quasi sempre | Solo per URL esterni |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Best Practices Blazor Server Navigation
|
|
||||||
|
|
||||||
### ? DO
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Navigazione normale (99% dei casi)
|
|
||||||
Navigation.NavigateTo("/somewhere");
|
|
||||||
|
|
||||||
// Con returnUrl
|
|
||||||
Navigation.NavigateTo(returnUrl ?? "/");
|
|
||||||
|
|
||||||
// In event handler
|
|
||||||
private void HandleClick()
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo("/page");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dopo operazione async
|
|
||||||
private async Task HandleSubmit()
|
|
||||||
{
|
|
||||||
await SaveDataAsync();
|
|
||||||
Navigation.NavigateTo("/success");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ? DON'T
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// ? forceLoad in componente interattivo
|
|
||||||
Navigation.NavigateTo("/somewhere", forceLoad: true);
|
|
||||||
|
|
||||||
// ? forceLoad dopo SignIn
|
|
||||||
await SignInManager.PasswordSignInAsync(...);
|
|
||||||
Navigation.NavigateTo("/", forceLoad: true); // ERRORE!
|
|
||||||
|
|
||||||
// ? forceLoad in event handler
|
|
||||||
private void HandleClick()
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo("/page", forceLoad: true); // ERRORE!
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ? Quando forceLoad È OK
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Solo per navigazione a URL ESTERNI
|
|
||||||
Navigation.NavigateTo("https://external-site.com", forceLoad: true);
|
|
||||||
|
|
||||||
// Solo per download file
|
|
||||||
Navigation.NavigateTo("/api/download/file.pdf", forceLoad: true);
|
|
||||||
|
|
||||||
// Solo per logout completo (opzionale)
|
|
||||||
await SignInManager.SignOutAsync();
|
|
||||||
Navigation.NavigateTo("/login", forceLoad: true); // OK ma non necessario
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Approfondimento: Headers Read-Only
|
|
||||||
|
|
||||||
### Cos'è l'errore?
|
|
||||||
|
|
||||||
```
|
|
||||||
Headers are read-only, response has already started.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Significa:**
|
|
||||||
|
|
||||||
1. Server ha già iniziato a inviare risposta HTTP al client
|
|
||||||
2. Header HTTP già inviati
|
|
||||||
3. Tentativo di modificare header (es. `Set-Cookie`, `Location`)
|
|
||||||
4. ? Impossibile - header già inviati!
|
|
||||||
|
|
||||||
### Quando Succede in Blazor Server?
|
|
||||||
|
|
||||||
```
|
|
||||||
Ciclo Richiesta/Risposta HTTP:
|
|
||||||
|
|
||||||
1. Browser ? GET /login
|
|
||||||
2. Server ? Invia header (Content-Type, etc.)
|
|
||||||
3. Server ? Invia HTML (pagina Login)
|
|
||||||
4. ? Risposta HTTP completata
|
|
||||||
|
|
||||||
Interazione SignalR:
|
|
||||||
|
|
||||||
5. JavaScript ? Connessione SignalR
|
|
||||||
6. Utente clicca "Accedi"
|
|
||||||
7. SignalR ? Esegue HandleLogin()
|
|
||||||
8. SignInManager crea cookie
|
|
||||||
9. forceLoad: true tenta di modificare header
|
|
||||||
10. ? ERRORE: header già inviati al punto 2!
|
|
||||||
```
|
|
||||||
|
|
||||||
### Perché forceLoad: false Funziona?
|
|
||||||
|
|
||||||
```
|
|
||||||
Con forceLoad: false (default):
|
|
||||||
|
|
||||||
1-4. (come sopra)
|
|
||||||
5. SignalR connessione
|
|
||||||
6. Utente clicca "Accedi"
|
|
||||||
7. SignalR ? Esegue HandleLogin()
|
|
||||||
8. SignInManager crea cookie (già funziona via SignalR)
|
|
||||||
9. Navigation.NavigateTo("/") via SignalR
|
|
||||||
10. ? Blazor gestisce navigazione senza modificare header HTTP
|
|
||||||
11. ? Funziona!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Checklist Finale
|
|
||||||
|
|
||||||
- [x] **Rimosso forceLoad da HandleLogin** - Fix principale
|
|
||||||
- [x] **Verificato OnInitializedAsync** - Già corretto
|
|
||||||
- [x] **Build riuscita** - Nessun errore compilazione
|
|
||||||
- [x] **Test funzionali** - Login funziona ?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Modificato
|
|
||||||
|
|
||||||
| File | Modifica | Motivo |
|
|
||||||
|------|----------|--------|
|
|
||||||
| `Pages/Login.razor` | Rimosso `forceLoad: true` | Evita errore "Headers are read-only" |
|
|
||||||
|
|
||||||
**Riga modificata:**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Prima:
|
|
||||||
Navigation.NavigateTo(ReturnUrl ?? "/", forceLoad: true);
|
|
||||||
|
|
||||||
// Dopo:
|
|
||||||
Navigation.NavigateTo(ReturnUrl ?? "/");
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Test Completo
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Build
|
|
||||||
dotnet build
|
|
||||||
|
|
||||||
# 2. Run
|
|
||||||
dotnet run
|
|
||||||
|
|
||||||
# 3. Browser
|
|
||||||
http://localhost:5000
|
|
||||||
|
|
||||||
# 4. Redirect a /login
|
|
||||||
|
|
||||||
# 5. Login
|
|
||||||
Username: admin
|
|
||||||
Password: Admin@Password123!
|
|
||||||
|
|
||||||
# 6. Click "Accedi"
|
|
||||||
? Nessun errore
|
|
||||||
? Redirect a homepage
|
|
||||||
? Autenticato correttamente
|
|
||||||
? Sidebar visibile
|
|
||||||
? Menu funzionante
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Riferimenti
|
|
||||||
|
|
||||||
**ASP.NET Core Blazor Navigation:**
|
|
||||||
- [NavigationManager.NavigateTo](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto)
|
|
||||||
- [Blazor Server Circuits](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/signalr)
|
|
||||||
|
|
||||||
**Headers Read-Only Error:**
|
|
||||||
- [HttpResponse Headers](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.headers)
|
|
||||||
- [Response Already Started](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**? PROBLEMA RISOLTO!**
|
|
||||||
|
|
||||||
- ? Errore "Headers are read-only" eliminato
|
|
||||||
- ? Login funziona correttamente
|
|
||||||
- ? Nessun forceLoad non necessario
|
|
||||||
- ? Best practices Blazor Server applicate
|
|
||||||
- ? Navigazione via SignalR (più veloce)
|
|
||||||
|
|
||||||
**?? Login pronto per production!**
|
|
||||||
|
|
||||||
### Test Finale Rapido
|
|
||||||
|
|
||||||
```
|
|
||||||
1. dotnet run
|
|
||||||
2. http://localhost:5000
|
|
||||||
3. Login: admin / Admin@Password123!
|
|
||||||
4. ? Funziona!
|
|
||||||
```
|
|
||||||
@@ -1,408 +0,0 @@
|
|||||||
# ? FIX: Layout Login Pulito + NavigationException Risolta
|
|
||||||
|
|
||||||
## ?? Problemi Risolti
|
|
||||||
|
|
||||||
### 1. ? Sidebar Visibile nella Pagina Login
|
|
||||||
**Prima:** La pagina di login mostrava sidebar e menu dell'applicazione anche se l'utente non era autenticato.
|
|
||||||
|
|
||||||
**Dopo:** Pagina login completamente pulita, solo il form di login senza elementi dell'interfaccia principale.
|
|
||||||
|
|
||||||
### 2. ?? NavigationException nel Debugger
|
|
||||||
**Prima:** L'eccezione appariva nei log di debug (anche se normale):
|
|
||||||
```
|
|
||||||
Microsoft.AspNetCore.Components.NavigationException
|
|
||||||
in Microsoft.AspNetCore.Components.Server.Circuits.RemoteNavigationManager.NavigateToCore
|
|
||||||
```
|
|
||||||
|
|
||||||
**Dopo:** Nessuna eccezione, redirect pulito senza warning.
|
|
||||||
|
|
||||||
### 3. ?? Box Credenziali Default Rimosso
|
|
||||||
**Prima:** Box giallo con credenziali di default visibile nella pagina login.
|
|
||||||
|
|
||||||
**Dopo:** Pagina login pulita senza warning o box informativi.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Modifiche Applicate
|
|
||||||
|
|
||||||
### 1. Creato `Shared/LoginLayout.razor`
|
|
||||||
|
|
||||||
**Layout pulito senza sidebar:**
|
|
||||||
|
|
||||||
```razor
|
|
||||||
@inherits LayoutComponentBase
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="it">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<base href="~/" />
|
|
||||||
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
|
|
||||||
<link rel="stylesheet" href="css/app.css" />
|
|
||||||
<link rel="stylesheet" href="AutoBidder.styles.css" />
|
|
||||||
<link rel="icon" type="image/png" href="favicon.ico" />
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
|
||||||
<HeadOutlet />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
@Body
|
|
||||||
|
|
||||||
<script src="_framework/blazor.server.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Caratteristiche:**
|
|
||||||
- ? Solo contenuto HTML essenziale
|
|
||||||
- ? Nessuna sidebar o menu
|
|
||||||
- ? Nessun componente MainLayout
|
|
||||||
- ? Stili Bootstrap e app.css caricati
|
|
||||||
- ? Bootstrap Icons caricati
|
|
||||||
|
|
||||||
### 2. Modificato `Pages/Login.razor`
|
|
||||||
|
|
||||||
**Aggiunto layout pulito:**
|
|
||||||
```razor
|
|
||||||
@page "/login"
|
|
||||||
@layout LoginLayout // ? NUOVO: Usa layout senza sidebar
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rimosso box credenziali:**
|
|
||||||
```razor
|
|
||||||
// RIMOSSO:
|
|
||||||
@if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ADMIN_PASSWORD")))
|
|
||||||
{
|
|
||||||
<div class="mt-3 p-3 bg-warning ...">
|
|
||||||
<p>Credenziali di default:</p>
|
|
||||||
...
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Migliorato `Shared/RedirectToLogin.razor`
|
|
||||||
|
|
||||||
**Prima (causava NavigationException):**
|
|
||||||
```razor
|
|
||||||
@code {
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo("/login", forceLoad: true); // ? Causava eccezione
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Dopo (redirect pulito):**
|
|
||||||
```razor
|
|
||||||
<div class="d-flex justify-content-center align-items-center" style="min-height: 100vh;">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="visually-hidden">Reindirizzamento...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
// Redirect senza forceLoad = nessuna eccezione
|
|
||||||
var returnUrl = Navigation.Uri.Replace(Navigation.BaseUri.TrimEnd('/'), "");
|
|
||||||
var loginUrl = $"/login?returnUrl={Uri.EscapeDataString(returnUrl)}";
|
|
||||||
Navigation.NavigateTo(loginUrl); // ? Nessuna eccezione!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Vantaggi:**
|
|
||||||
- ? Nessuna `NavigationException`
|
|
||||||
- ? Spinner visibile durante redirect
|
|
||||||
- ? Preserva `returnUrl` per redirect post-login
|
|
||||||
- ? Esperienza utente migliore
|
|
||||||
|
|
||||||
### 4. Aggiornato `Pages/Logout.razor`
|
|
||||||
|
|
||||||
**Aggiunto layout pulito:**
|
|
||||||
```razor
|
|
||||||
@page "/logout"
|
|
||||||
@layout LoginLayout // ? NUOVO: Usa layout senza sidebar
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rimosso forceLoad:**
|
|
||||||
```razor
|
|
||||||
@code {
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
await SignInManager.SignOutAsync();
|
|
||||||
Navigation.NavigateTo("/login"); // ? Senza forceLoad
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Esperienza Utente Finale
|
|
||||||
|
|
||||||
### Flusso Login
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Utente apre http://localhost:5000
|
|
||||||
?
|
|
||||||
2. Non autenticato ? RedirectToLogin
|
|
||||||
?
|
|
||||||
3. Spinner "Reindirizzamento..." (100vh fullscreen)
|
|
||||||
?
|
|
||||||
4. Redirect a /login
|
|
||||||
?
|
|
||||||
5. ? PAGINA LOGIN PULITA:
|
|
||||||
- Sfondo gradiente
|
|
||||||
- Card login centrata
|
|
||||||
- NO sidebar
|
|
||||||
- NO menu
|
|
||||||
- Solo form username/password
|
|
||||||
?
|
|
||||||
6. Inserisce credenziali ? Login
|
|
||||||
?
|
|
||||||
7. Redirect a homepage
|
|
||||||
?
|
|
||||||
8. ? Sidebar e menu APPAIONO SOLO ORA
|
|
||||||
```
|
|
||||||
|
|
||||||
### Flusso Logout
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Click "Logout" in sidebar
|
|
||||||
?
|
|
||||||
2. Redirect a /logout
|
|
||||||
?
|
|
||||||
3. Pagina pulita con spinner "Disconnessione..."
|
|
||||||
?
|
|
||||||
4. Cookie distrutto
|
|
||||||
?
|
|
||||||
5. Redirect a /login
|
|
||||||
?
|
|
||||||
6. ? Pagina login pulita (nessuna sidebar)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Confronto Prima/Dopo
|
|
||||||
|
|
||||||
### Prima (Problematico)
|
|
||||||
|
|
||||||
| Aspetto | Problema |
|
|
||||||
|---------|----------|
|
|
||||||
| **Layout** | Sidebar visibile anche non autenticati |
|
|
||||||
| **NavigationException** | Eccezione nei log debug |
|
|
||||||
| **Box Warning** | Credenziali default visibili |
|
|
||||||
| **Esperienza** | Confusa, elementi UI non necessari |
|
|
||||||
|
|
||||||
### Dopo (Risolto)
|
|
||||||
|
|
||||||
| Aspetto | Soluzione |
|
|
||||||
|---------|-----------|
|
|
||||||
| **Layout** | ? Pagina login completamente pulita |
|
|
||||||
| **NavigationException** | ? Nessuna eccezione, redirect pulito |
|
|
||||||
| **Box Warning** | ? Rimosso, interfaccia minimal |
|
|
||||||
| **Esperienza** | ? Professionale, focus sul login |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Test Completi
|
|
||||||
|
|
||||||
### Test 1: Primo Avvio
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Avvia: dotnet run
|
|
||||||
2. Browser: http://localhost:5000
|
|
||||||
3. ? Spinner "Reindirizzamento..." appare
|
|
||||||
4. ? Redirect automatico a /login
|
|
||||||
5. ? Pagina login PULITA (nessuna sidebar)
|
|
||||||
6. ? Nessuna eccezione nei log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 2: Login
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Pagina login
|
|
||||||
2. Username: admin
|
|
||||||
3. Password: Admin@Password123!
|
|
||||||
4. Click "Accedi"
|
|
||||||
5. ? Redirect a homepage
|
|
||||||
6. ? Sidebar e menu APPAIONO ORA
|
|
||||||
7. ? Dashboard funzionante
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 3: Accesso Pagina Protetta
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Logout
|
|
||||||
2. Browser: http://localhost:5000/settings
|
|
||||||
3. ? Spinner "Reindirizzamento..."
|
|
||||||
4. ? Redirect a /login?returnUrl=%2Fsettings
|
|
||||||
5. ? Login
|
|
||||||
6. ? Redirect automatico a /settings
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 4: Logout
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Click "Logout" in sidebar
|
|
||||||
2. ? Pagina logout pulita con spinner
|
|
||||||
3. ? "Disconnessione in corso..."
|
|
||||||
4. ? Redirect a /login
|
|
||||||
5. ? Pagina login pulita (nessuna sidebar)
|
|
||||||
6. ? Cookie distrutto
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Modificati
|
|
||||||
|
|
||||||
| File | Modifiche | Motivo |
|
|
||||||
|------|-----------|--------|
|
|
||||||
| **Shared/LoginLayout.razor** | ? NUOVO | Layout pulito senza sidebar |
|
|
||||||
| **Pages/Login.razor** | `@layout LoginLayout` + rimosso box | Interfaccia pulita |
|
|
||||||
| **Shared/RedirectToLogin.razor** | Rimosso `forceLoad`, aggiunto spinner | Nessuna eccezione |
|
|
||||||
| **Pages/Logout.razor** | `@layout LoginLayout` + rimosso `forceLoad` | Consistenza UI |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Vantaggi della Soluzione
|
|
||||||
|
|
||||||
### 1. UX Professionale
|
|
||||||
|
|
||||||
- ? Pagina login dedicata e pulita
|
|
||||||
- ? Nessun elemento UI confusionario
|
|
||||||
- ? Focus totale sul login
|
|
||||||
- ? Spinner informativi durante redirect
|
|
||||||
|
|
||||||
### 2. Sviluppo Pulito
|
|
||||||
|
|
||||||
- ? Nessuna eccezione nei log
|
|
||||||
- ? Debug più facile
|
|
||||||
- ? Codice più manutenibile
|
|
||||||
- ? Separazione chiara login/app
|
|
||||||
|
|
||||||
### 3. Sicurezza Mantenuta
|
|
||||||
|
|
||||||
- ? Autenticazione obbligatoria
|
|
||||||
- ? Redirect automatico
|
|
||||||
- ? ReturnUrl preservato
|
|
||||||
- ? Cookie sicuri
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Dettagli Tecnici
|
|
||||||
|
|
||||||
### LoginLayout vs MainLayout
|
|
||||||
|
|
||||||
```
|
|
||||||
LoginLayout:
|
|
||||||
- Solo HTML base
|
|
||||||
- Nessun componente UI
|
|
||||||
- Fullscreen form
|
|
||||||
- Ideale per auth pages
|
|
||||||
|
|
||||||
MainLayout:
|
|
||||||
- Sidebar + menu
|
|
||||||
- Dashboard components
|
|
||||||
- App navigation
|
|
||||||
- Ideale per pagine protette
|
|
||||||
```
|
|
||||||
|
|
||||||
### Redirect Senza forceLoad
|
|
||||||
|
|
||||||
**Perché funziona?**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// PRIMA (con eccezione):
|
|
||||||
Navigation.NavigateTo("/login", forceLoad: true);
|
|
||||||
// forceLoad causa NavigationException (normale ma fastidioso)
|
|
||||||
|
|
||||||
// DOPO (senza eccezione):
|
|
||||||
Navigation.NavigateTo("/login");
|
|
||||||
// Blazor gestisce il redirect internamente, nessuna eccezione
|
|
||||||
```
|
|
||||||
|
|
||||||
**Quando forceLoad è necessario?**
|
|
||||||
|
|
||||||
- ? Mai per redirect interni Blazor
|
|
||||||
- ? Solo per URL esterni o download file
|
|
||||||
- ? Solo se serve refresh completo browser
|
|
||||||
|
|
||||||
### ReturnUrl Preservato
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var returnUrl = Navigation.Uri.Replace(Navigation.BaseUri.TrimEnd('/'), "");
|
|
||||||
var loginUrl = $"/login?returnUrl={Uri.EscapeDataString(returnUrl)}";
|
|
||||||
Navigation.NavigateTo(loginUrl);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Esempio:**
|
|
||||||
```
|
|
||||||
Utente va a: /settings (non autenticato)
|
|
||||||
Redirect a: /login?returnUrl=%2Fsettings
|
|
||||||
Dopo login: redirect automatico a /settings ?
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Checklist Completa
|
|
||||||
|
|
||||||
- [x] **LoginLayout creato** - Layout pulito senza sidebar
|
|
||||||
- [x] **Login.razor aggiornato** - Usa LoginLayout + rimosso box
|
|
||||||
- [x] **RedirectToLogin migliorato** - Nessuna eccezione + spinner
|
|
||||||
- [x] **Logout.razor aggiornato** - Usa LoginLayout + redirect pulito
|
|
||||||
- [x] **Build verificata** - Compilazione riuscita ?
|
|
||||||
- [x] **NavigationException eliminata** - Log puliti ?
|
|
||||||
- [x] **UX migliorata** - Pagina login professionale ?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Prossimi Passi
|
|
||||||
|
|
||||||
### Test Locale
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Build
|
|
||||||
dotnet build
|
|
||||||
|
|
||||||
# 2. Run
|
|
||||||
dotnet run
|
|
||||||
|
|
||||||
# 3. Browser
|
|
||||||
http://localhost:5000
|
|
||||||
|
|
||||||
# 4. Verifica:
|
|
||||||
# ? Pagina login pulita (nessuna sidebar)
|
|
||||||
# ? Nessuna eccezione nei log
|
|
||||||
# ? Login funzionante
|
|
||||||
# ? Sidebar appare DOPO login
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deploy Container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build immagine
|
|
||||||
docker build -t autobidder:1.2.0 .
|
|
||||||
|
|
||||||
# Test container
|
|
||||||
docker run -d -p 8889:8080 \
|
|
||||||
-e ADMIN_PASSWORD="Test123!@#" \
|
|
||||||
autobidder:1.2.0
|
|
||||||
|
|
||||||
# Verifica
|
|
||||||
http://localhost:8889
|
|
||||||
# ? Login pulito
|
|
||||||
# ? Nessuna eccezione
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**? TUTTO RISOLTO!**
|
|
||||||
|
|
||||||
- ? Pagina login completamente pulita (nessuna sidebar)
|
|
||||||
- ? NavigationException eliminata (log puliti)
|
|
||||||
- ? Box credenziali rimosso (interfaccia minimal)
|
|
||||||
- ? UX professionale e consistente
|
|
||||||
- ? Codice manutenibile e pulito
|
|
||||||
|
|
||||||
**?? Pronto per il deploy production!**
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
# ?? FIX: Schermata Login Non Appare
|
|
||||||
|
|
||||||
## ? Problema
|
|
||||||
|
|
||||||
Quando si avvia l'applicazione, invece di vedere la schermata di login, appariva direttamente la homepage (o pagina vuota).
|
|
||||||
|
|
||||||
**Causa:** Mancava il componente `AuthorizeRouteView` che gestisce il redirect automatico alla pagina di login per utenti non autenticati.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Soluzione Applicata
|
|
||||||
|
|
||||||
### 1. Aggiornato `App.razor`
|
|
||||||
|
|
||||||
**Prima (PROBLEMA):**
|
|
||||||
```razor
|
|
||||||
<Router AppAssembly="@typeof(App).Assembly">
|
|
||||||
<Found Context="routeData">
|
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
|
||||||
...
|
|
||||||
</Found>
|
|
||||||
</Router>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Dopo (RISOLTO):**
|
|
||||||
```razor
|
|
||||||
<CascadingAuthenticationState>
|
|
||||||
<Router AppAssembly="@typeof(App).Assembly">
|
|
||||||
<Found Context="routeData">
|
|
||||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
|
||||||
<NotAuthorized>
|
|
||||||
@if (context.User.Identity?.IsAuthenticated != true)
|
|
||||||
{
|
|
||||||
<RedirectToLogin />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<p>Non sei autorizzato.</p>
|
|
||||||
}
|
|
||||||
</NotAuthorized>
|
|
||||||
</AuthorizeRouteView>
|
|
||||||
...
|
|
||||||
</Found>
|
|
||||||
</Router>
|
|
||||||
</CascadingAuthenticationState>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Modifiche chiave:**
|
|
||||||
- ? `<CascadingAuthenticationState>` - Propaga stato autenticazione
|
|
||||||
- ? `<AuthorizeRouteView>` - Gestisce autorizzazione route
|
|
||||||
- ? `<NotAuthorized>` - Handler per utenti non autenticati
|
|
||||||
- ? `<RedirectToLogin />` - Componente redirect automatico
|
|
||||||
|
|
||||||
### 2. Creato `Shared/RedirectToLogin.razor`
|
|
||||||
|
|
||||||
```razor
|
|
||||||
@using Microsoft.AspNetCore.Components
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
|
|
||||||
@code {
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo("/login", forceLoad: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Funzione:** Redirect automatico e immediato a `/login` quando chiamato.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Come Funziona Ora
|
|
||||||
|
|
||||||
### Flusso Autenticazione
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Utente apre http://localhost:5000
|
|
||||||
?
|
|
||||||
2. App.razor ? AuthorizeRouteView controlla autenticazione
|
|
||||||
?
|
|
||||||
3. Utente NON autenticato?
|
|
||||||
?
|
|
||||||
4. <NotAuthorized> ? <RedirectToLogin />
|
|
||||||
?
|
|
||||||
5. NavigationManager.NavigateTo("/login", forceLoad: true)
|
|
||||||
?
|
|
||||||
6. ? Pagina Login.razor appare
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dopo Login
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Utente inserisce username/password
|
|
||||||
?
|
|
||||||
2. SignInManager.PasswordSignInAsync() ? Success
|
|
||||||
?
|
|
||||||
3. Cookie autenticazione creato
|
|
||||||
?
|
|
||||||
4. Navigation.NavigateTo("/", forceLoad: true)
|
|
||||||
?
|
|
||||||
5. AuthorizeRouteView ? Utente autenticato ?
|
|
||||||
?
|
|
||||||
6. ? Homepage AutoBidder carica
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Test della Correzione
|
|
||||||
|
|
||||||
### Test 1: Primo Avvio (Non Autenticato)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Avvia applicazione: dotnet run
|
|
||||||
2. Browser: http://localhost:8080
|
|
||||||
3. Risultato atteso: Redirect automatico a /login ?
|
|
||||||
4. Vedi: Pagina login con form username/password ?
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 2: Login Riuscito
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Pagina login
|
|
||||||
2. Username: admin
|
|
||||||
3. Password: (ADMIN_PASSWORD configurata)
|
|
||||||
4. Click "Accedi"
|
|
||||||
5. Risultato: Redirect a homepage ?
|
|
||||||
6. Vedi: Dashboard AutoBidder ?
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 3: Sessione Persistente
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Login effettuato
|
|
||||||
2. Chiudi browser
|
|
||||||
3. Riapri dopo 5 minuti
|
|
||||||
4. Vai a http://localhost:8080
|
|
||||||
5. Risultato: Homepage (già autenticato, cookie valido) ?
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 4: Logout
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Click logout in sidebar
|
|
||||||
2. Risultato: Redirect a /login ?
|
|
||||||
3. Cookie distrutto
|
|
||||||
4. Prova ad andare su homepage
|
|
||||||
5. Risultato: Redirect a /login ?
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Modificati
|
|
||||||
|
|
||||||
| File | Modifica | Motivo |
|
|
||||||
|------|----------|--------|
|
|
||||||
| `App.razor` | Aggiunto `AuthorizeRouteView` + `CascadingAuthenticationState` | Gestione autorizzazione route |
|
|
||||||
| `Shared/RedirectToLogin.razor` | Nuovo componente | Redirect automatico a login |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Troubleshooting
|
|
||||||
|
|
||||||
### Problema: Ancora non vedo login
|
|
||||||
|
|
||||||
**Verifica:**
|
|
||||||
|
|
||||||
1. **Build riuscita?**
|
|
||||||
```bash
|
|
||||||
dotnet build
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Browser cache?**
|
|
||||||
```
|
|
||||||
CTRL+SHIFT+R (hard refresh)
|
|
||||||
Oppure: F12 ? Network ? Disable cache
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Cookie esistente?**
|
|
||||||
```
|
|
||||||
F12 ? Application ? Cookies
|
|
||||||
Elimina tutti i cookie per localhost
|
|
||||||
Ricarica pagina
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problema: Loop infinito redirect
|
|
||||||
|
|
||||||
**Causa:** Pagina `/login` ha `[Authorize]`
|
|
||||||
|
|
||||||
**Verifica:**
|
|
||||||
```csharp
|
|
||||||
// Pages/Login.razor
|
|
||||||
@page "/login"
|
|
||||||
// NON deve avere: @attribute [Authorize]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problema: 404 su /login
|
|
||||||
|
|
||||||
**Verifica routing:**
|
|
||||||
```csharp
|
|
||||||
// Program.cs
|
|
||||||
app.MapBlazorHub();
|
|
||||||
app.MapFallbackToPage("/_Host");
|
|
||||||
```
|
|
||||||
|
|
||||||
Deve essere presente e in quest'ordine.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Risultato Finale
|
|
||||||
|
|
||||||
**Comportamento corretto:**
|
|
||||||
|
|
||||||
| Scenario | Risultato |
|
|
||||||
|----------|-----------|
|
|
||||||
| Primo accesso (non autenticato) | ? Redirect automatico a `/login` |
|
|
||||||
| Login riuscito | ? Redirect a homepage |
|
|
||||||
| Accesso a pagina protetta (non autenticato) | ? Redirect a `/login` |
|
|
||||||
| Logout | ? Redirect a `/login` |
|
|
||||||
| Sessione valida | ? Accesso diretto homepage |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Riferimenti
|
|
||||||
|
|
||||||
**ASP.NET Core Blazor Authentication:**
|
|
||||||
- [AuthorizeRouteView](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/)
|
|
||||||
- [CascadingAuthenticationState](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/)
|
|
||||||
|
|
||||||
**Identity Cookie Authentication:**
|
|
||||||
- [Cookie Authentication](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**? FIX APPLICATO - Login appare correttamente all'avvio!**
|
|
||||||
|
|
||||||
Ora quando avvii l'applicazione:
|
|
||||||
1. ? Vedi immediatamente la schermata di login
|
|
||||||
2. ? Inserisci username/password
|
|
||||||
3. ? Accedi alla dashboard AutoBidder
|
|
||||||
|
|
||||||
**?? Autenticazione funzionante al 100%!**
|
|
||||||
@@ -1,418 +0,0 @@
|
|||||||
# ? FIX: NavigationException in RedirectToLogin Risolto
|
|
||||||
|
|
||||||
## ?? Errore Originale
|
|
||||||
|
|
||||||
```
|
|
||||||
Microsoft.AspNetCore.Components.NavigationException
|
|
||||||
HResult=0x80131500
|
|
||||||
Messaggio=Exception of type 'Microsoft.AspNetCore.Components.NavigationException' was thrown.
|
|
||||||
Origine=Microsoft.AspNetCore.Components.Server
|
|
||||||
Analisi dello stack:
|
|
||||||
in Microsoft.AspNetCore.Components.Server.Circuits.RemoteNavigationManager.NavigateToCore(String uri, NavigationOptions options)
|
|
||||||
in Microsoft.AspNetCore.Components.NavigationManager.NavigateToCore(String uri, Boolean forceLoad)
|
|
||||||
in Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(String uri, Boolean forceLoad, Boolean replace)
|
|
||||||
in AutoBidder.Shared.RedirectToLogin.OnInitialized()
|
|
||||||
```
|
|
||||||
|
|
||||||
**Linea problematica:**
|
|
||||||
```csharp
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo(loginUrl); // ? ECCEZIONE QUI!
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Causa del Problema
|
|
||||||
|
|
||||||
**Blazor Server Circuit Lifecycle:**
|
|
||||||
|
|
||||||
```
|
|
||||||
1. OnInitialized() chiamato
|
|
||||||
?
|
|
||||||
2. Componente NON ancora renderizzato
|
|
||||||
?
|
|
||||||
3. Circuito SignalR NON completamente inizializzato
|
|
||||||
?
|
|
||||||
4. NavigateTo() richiede circuito attivo
|
|
||||||
?
|
|
||||||
5. ? NavigationException viene lanciata
|
|
||||||
```
|
|
||||||
|
|
||||||
**Perché l'eccezione?**
|
|
||||||
|
|
||||||
In Blazor Server, `OnInitialized()` viene eseguito **prima** che il componente sia renderizzato e **prima** che la connessione SignalR sia completamente stabilita. Quando si chiama `NavigateTo()` in questa fase, il framework lancia `NavigationException` perché il circuito non è pronto per gestire la navigazione.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Soluzione Applicata
|
|
||||||
|
|
||||||
### OnInitialized ? OnAfterRenderAsync
|
|
||||||
|
|
||||||
**Prima (PROBLEMATICO):**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
// Eseguito PRIMA del rendering
|
|
||||||
// Circuito SignalR NON ancora pronto
|
|
||||||
Navigation.NavigateTo(loginUrl); // ? ECCEZIONE!
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Dopo (CORRETTO):**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
private bool _hasRedirected = false;
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender && !_hasRedirected)
|
|
||||||
{
|
|
||||||
_hasRedirected = true;
|
|
||||||
|
|
||||||
// Eseguito DOPO il rendering
|
|
||||||
// Circuito SignalR completamente inizializzato
|
|
||||||
Navigation.NavigateTo(loginUrl); // ? NESSUNA ECCEZIONE!
|
|
||||||
}
|
|
||||||
|
|
||||||
await base.OnAfterRenderAsync(firstRender);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Come Funziona la Soluzione
|
|
||||||
|
|
||||||
### Lifecycle Corretto
|
|
||||||
|
|
||||||
```
|
|
||||||
1. OnInitialized() eseguito
|
|
||||||
?
|
|
||||||
2. Componente renderizzato (spinner visibile)
|
|
||||||
?
|
|
||||||
3. Circuito SignalR completamente attivo
|
|
||||||
?
|
|
||||||
4. OnAfterRenderAsync(firstRender: true) chiamato
|
|
||||||
?
|
|
||||||
5. Navigation.NavigateTo() eseguito
|
|
||||||
?
|
|
||||||
6. ? Redirect funziona senza eccezioni
|
|
||||||
```
|
|
||||||
|
|
||||||
### Flag _hasRedirected
|
|
||||||
|
|
||||||
**Perché serve?**
|
|
||||||
|
|
||||||
`OnAfterRenderAsync` può essere chiamato **più volte** durante il ciclo di vita del componente:
|
|
||||||
- Primo rendering: `firstRender = true`
|
|
||||||
- Re-rendering successivi: `firstRender = false`
|
|
||||||
|
|
||||||
Il flag `_hasRedirected` assicura che il redirect avvenga **una sola volta**, anche se il componente viene ri-renderizzato.
|
|
||||||
|
|
||||||
**Esempio scenario:**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// SENZA flag (PROBLEMATICO):
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender)
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo(loginUrl);
|
|
||||||
// Se il componente si re-renderizza, questo codice
|
|
||||||
// verrebbe eseguito di nuovo! ?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CON flag (CORRETTO):
|
|
||||||
private bool _hasRedirected = false;
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender && !_hasRedirected)
|
|
||||||
{
|
|
||||||
_hasRedirected = true;
|
|
||||||
Navigation.NavigateTo(loginUrl);
|
|
||||||
// Anche se re-render, non esegue più ?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Confronto Lifecycle Methods
|
|
||||||
|
|
||||||
### OnInitialized vs OnAfterRenderAsync
|
|
||||||
|
|
||||||
| Aspetto | OnInitialized | OnAfterRenderAsync |
|
|
||||||
|---------|---------------|-------------------|
|
|
||||||
| **Quando** | Prima del rendering | Dopo il rendering |
|
|
||||||
| **Circuito SignalR** | ? Non attivo | ? Completamente attivo |
|
|
||||||
| **DOM disponibile** | ? No | ? Sì |
|
|
||||||
| **NavigateTo sicuro** | ? No (eccezione) | ? Sì (funziona) |
|
|
||||||
| **JSInterop sicuro** | ? No | ? Sì |
|
|
||||||
| **Chiamato quante volte** | 1 volta | Ogni rendering |
|
|
||||||
|
|
||||||
### Quando Usare Quale
|
|
||||||
|
|
||||||
**OnInitialized / OnInitializedAsync:**
|
|
||||||
- ? Caricare dati dal database
|
|
||||||
- ? Inizializzare state del componente
|
|
||||||
- ? Configurare parametri
|
|
||||||
- ? NavigateTo
|
|
||||||
- ? JSInterop
|
|
||||||
|
|
||||||
**OnAfterRenderAsync:**
|
|
||||||
- ? NavigateTo
|
|
||||||
- ? JSInterop (focus, scroll, etc.)
|
|
||||||
- ? Interazioni con DOM
|
|
||||||
- ? Inizializzare librerie JavaScript
|
|
||||||
- ? Caricare dati pesanti (rallenta rendering)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Test della Soluzione
|
|
||||||
|
|
||||||
### Test 1: Primo Avvio
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Avvia: dotnet run
|
|
||||||
2. Browser: http://localhost:5000
|
|
||||||
3. ? Spinner "Reindirizzamento..." appare
|
|
||||||
4. ? Nessuna NavigationException
|
|
||||||
5. ? Redirect a /login funziona
|
|
||||||
6. ? Pagina login carica correttamente
|
|
||||||
```
|
|
||||||
|
|
||||||
**Log attesi:**
|
|
||||||
```
|
|
||||||
Microsoft.Hosting.Lifetime: Information: Now listening on: http://localhost:5000
|
|
||||||
Microsoft.Hosting.Lifetime: Information: Application started
|
|
||||||
(NESSUNA ECCEZIONE) ?
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 2: Accesso Pagina Protetta (Non Autenticato)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Browser: http://localhost:5000/settings
|
|
||||||
2. ? Spinner appare
|
|
||||||
3. ? Nessuna eccezione
|
|
||||||
4. ? Redirect a /login?returnUrl=%2Fsettings
|
|
||||||
5. ? Login funzionante
|
|
||||||
6. ? Dopo login: redirect automatico a /settings
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 3: Debug con Breakpoint
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Breakpoint su riga 15 (OnAfterRenderAsync)
|
|
||||||
2. F5 debug
|
|
||||||
3. ? Breakpoint colpito DOPO rendering
|
|
||||||
4. ? firstRender = true
|
|
||||||
5. ? _hasRedirected = false
|
|
||||||
6. F10 (step over)
|
|
||||||
7. ? NavigateTo eseguito senza eccezioni
|
|
||||||
8. ? _hasRedirected ora = true
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Risultato Finale
|
|
||||||
|
|
||||||
### Prima (con NavigationException)
|
|
||||||
|
|
||||||
```
|
|
||||||
? Eccezione al primo avvio
|
|
||||||
? Stack trace nel debugger
|
|
||||||
? Log inquinati con errori
|
|
||||||
? Esperienza utente degradata (anche se funziona)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dopo (senza eccezioni)
|
|
||||||
|
|
||||||
```
|
|
||||||
? Nessuna eccezione
|
|
||||||
? Log puliti
|
|
||||||
? Debugger senza errori
|
|
||||||
? Esperienza utente fluida
|
|
||||||
? Codice idiomatico Blazor
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Best Practices Blazor Navigation
|
|
||||||
|
|
||||||
### ? DO
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// In OnAfterRenderAsync per redirect
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender)
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo("/somewhere");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In event handler
|
|
||||||
private void HandleClick()
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo("/somewhere");
|
|
||||||
}
|
|
||||||
|
|
||||||
// In async lifecycle method con await
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
await LoadDataAsync();
|
|
||||||
// NavigateTo solo se necessario dopo load
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ? DON'T
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// ? Mai NavigateTo in OnInitialized
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo("/somewhere"); // ECCEZIONE!
|
|
||||||
}
|
|
||||||
|
|
||||||
// ? Mai NavigateTo in costruttore
|
|
||||||
public MyComponent()
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo("/somewhere"); // ECCEZIONE!
|
|
||||||
}
|
|
||||||
|
|
||||||
// ? NavigateTo senza controllo in OnAfterRenderAsync
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
// Senza flag, redirect multipli!
|
|
||||||
Navigation.NavigateTo("/somewhere");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Approfondimento: Blazor Server Circuit
|
|
||||||
|
|
||||||
### Cos'è il Circuit?
|
|
||||||
|
|
||||||
Il **Circuit** è la connessione persistente tra client e server in Blazor Server:
|
|
||||||
|
|
||||||
```
|
|
||||||
Browser Server
|
|
||||||
| |
|
|
||||||
|-- SignalR Hub ----->|
|
|
||||||
|<-- Eventi UI --------|
|
|
||||||
|-- User Input ------->|
|
|
||||||
|<-- DOM Updates ------|
|
|
||||||
| |
|
|
||||||
[Circuit Attivo]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lifecycle del Circuit
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Browser richiede pagina
|
|
||||||
?
|
|
||||||
2. Server renderizza HTML statico
|
|
||||||
?
|
|
||||||
3. Browser carica blazor.server.js
|
|
||||||
?
|
|
||||||
4. JavaScript avvia connessione SignalR
|
|
||||||
?
|
|
||||||
5. Server crea Circuit
|
|
||||||
?
|
|
||||||
6. OnInitialized() chiamato
|
|
||||||
? (Circuit NON ancora completamente attivo)
|
|
||||||
7. Componente renderizzato
|
|
||||||
?
|
|
||||||
8. Circuit completamente attivo
|
|
||||||
?
|
|
||||||
9. OnAfterRenderAsync(firstRender: true) chiamato
|
|
||||||
? (? SICURO per NavigateTo)
|
|
||||||
10. App interattiva
|
|
||||||
```
|
|
||||||
|
|
||||||
### Perché NavigateTo Richiede Circuit Attivo?
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
Navigation.NavigateTo("/login");
|
|
||||||
```
|
|
||||||
|
|
||||||
Internamente fa:
|
|
||||||
1. Serializza URL
|
|
||||||
2. Invia messaggio via SignalR
|
|
||||||
3. Server processa navigazione
|
|
||||||
4. Invia aggiornamento DOM via SignalR
|
|
||||||
5. Browser applica cambiamenti
|
|
||||||
|
|
||||||
**Se Circuit non attivo:**
|
|
||||||
- ? SignalR non può inviare messaggi
|
|
||||||
- ? `NavigationException` viene lanciata
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Modificato
|
|
||||||
|
|
||||||
| File | Modifica | Motivo |
|
|
||||||
|------|----------|--------|
|
|
||||||
| `Shared/RedirectToLogin.razor` | `OnInitialized` ? `OnAfterRenderAsync` | Evita NavigationException |
|
|
||||||
|
|
||||||
**Codice aggiunto:**
|
|
||||||
- `private bool _hasRedirected` - Flag per singolo redirect
|
|
||||||
- `OnAfterRenderAsync` - Lifecycle method corretto
|
|
||||||
- Controllo `firstRender && !_hasRedirected` - Sicurezza
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Checklist Finale
|
|
||||||
|
|
||||||
- [x] **NavigationException eliminata** - Nessun errore al primo avvio
|
|
||||||
- [x] **OnAfterRenderAsync usato** - Lifecycle method corretto
|
|
||||||
- [x] **Flag _hasRedirected** - Prevenzione redirect multipli
|
|
||||||
- [x] **Build riuscita** - Compilazione senza errori
|
|
||||||
- [x] **Test funzionali** - Redirect funziona correttamente
|
|
||||||
- [x] **Log puliti** - Nessuna eccezione nei log
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Deploy
|
|
||||||
|
|
||||||
**Pronto per:**
|
|
||||||
- ? Test locale
|
|
||||||
- ? Debug senza eccezioni
|
|
||||||
- ? Deploy container Docker
|
|
||||||
- ? Production Unraid
|
|
||||||
|
|
||||||
**Comandi test:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build
|
|
||||||
dotnet build
|
|
||||||
|
|
||||||
# Run
|
|
||||||
dotnet run
|
|
||||||
|
|
||||||
# Browser
|
|
||||||
http://localhost:5000
|
|
||||||
|
|
||||||
# Risultato:
|
|
||||||
? Spinner visibile
|
|
||||||
? Redirect a /login
|
|
||||||
? Nessuna eccezione
|
|
||||||
? Login funzionante
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**? PROBLEMA RISOLTO!**
|
|
||||||
|
|
||||||
- ? NavigationException eliminata
|
|
||||||
- ? Codice idiomatico Blazor
|
|
||||||
- ? Best practices applicate
|
|
||||||
- ? Log puliti
|
|
||||||
- ? Esperienza utente fluida
|
|
||||||
|
|
||||||
**?? Pronto per il deploy production!**
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
# ?? Fix: Container in ascolto su porta sbagliata
|
|
||||||
|
|
||||||
## ? Problema
|
|
||||||
|
|
||||||
**Sintomo:**
|
|
||||||
- Container si avvia senza errori
|
|
||||||
- Log mostra: `Now listening on: http://[::]:5000`
|
|
||||||
- Pagina non carica quando accedi a `http://localhost:5000`
|
|
||||||
- Port mapping: `5000:8080` (host:container)
|
|
||||||
|
|
||||||
**Causa:**
|
|
||||||
La configurazione esplicita di Kestrel nel `Program.cs` veniva sovrascritta da configurazioni di default, facendo ascoltare il server sulla porta 5000 invece che 8080.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Diagnosi
|
|
||||||
|
|
||||||
### Log Container
|
|
||||||
```
|
|
||||||
warn: Microsoft.AspNetCore.Server.Kestrel[0]
|
|
||||||
Overriding address(es) 'http://+:8080'. Binding to endpoints defined via IConfiguration and/or UseKestrel() instead.
|
|
||||||
info: Microsoft.Hosting.Lifetime[14]
|
|
||||||
Now listening on: http://[::]:5000 ? PROBLEMA QUI!
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configurazione Attesa
|
|
||||||
```dockerfile
|
|
||||||
# Dockerfile
|
|
||||||
ENV ASPNETCORE_URLS=http://+:8080
|
|
||||||
EXPOSE 8080
|
|
||||||
```
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# docker-compose.yml
|
|
||||||
ports:
|
|
||||||
- "5000:8080" # Host 5000 ? Container 8080
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configurazione Effettiva
|
|
||||||
```
|
|
||||||
Container ascolta su: 5000 ?
|
|
||||||
Port mapping cerca: 8080 ?
|
|
||||||
Risultato: MISMATCH!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Soluzione Applicata
|
|
||||||
|
|
||||||
### Prima (PROBLEMA)
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Program.cs
|
|
||||||
builder.WebHost.ConfigureKestrel(options =>
|
|
||||||
{
|
|
||||||
options.ListenAnyIP(8080); // ? Ignorato da Kestrel!
|
|
||||||
// ...
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problema:** La configurazione esplicita viene sovrascritta dalle impostazioni di default di Kestrel.
|
|
||||||
|
|
||||||
### Dopo (RISOLTO)
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Program.cs
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
// NON configurare esplicitamente HTTP (usa ASPNETCORE_URLS)
|
|
||||||
// Configura solo HTTPS se richiesto
|
|
||||||
var enableHttps = builder.Configuration.GetValue<bool>("Kestrel:EnableHttps", false);
|
|
||||||
|
|
||||||
if (enableHttps)
|
|
||||||
{
|
|
||||||
builder.WebHost.ConfigureKestrel(options =>
|
|
||||||
{
|
|
||||||
// Solo configurazione HTTPS (porta 8443)
|
|
||||||
// HTTP gestito da ASPNETCORE_URLS automaticamente
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Nessuna configurazione Kestrel
|
|
||||||
// ASPNETCORE_URLS=http://+:8080 gestisce tutto
|
|
||||||
Console.WriteLine($"[Kestrel] Listening on: {ASPNETCORE_URLS}");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefici:**
|
|
||||||
- ? `ASPNETCORE_URLS` controlla la porta HTTP
|
|
||||||
- ? Configurazione centralizzata nel Dockerfile
|
|
||||||
- ? Facile override con variabili ambiente
|
|
||||||
- ? Meno conflitti tra configurazioni
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Come Funziona Ora
|
|
||||||
|
|
||||||
### Precedenza Configurazione Kestrel
|
|
||||||
|
|
||||||
1. **ASPNETCORE_URLS** (da Dockerfile/env)
|
|
||||||
2. Configurazione IConfiguration
|
|
||||||
3. ~~UseKestrel() esplicito~~ (rimosso per HTTP)
|
|
||||||
|
|
||||||
### Flusso Startup
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Dockerfile ? ENV ASPNETCORE_URLS=http://+:8080
|
|
||||||
2. Container start
|
|
||||||
3. Program.cs ? NO configurazione esplicita HTTP
|
|
||||||
4. Kestrel legge ASPNETCORE_URLS
|
|
||||||
5. ? Ascolta su porta 8080
|
|
||||||
```
|
|
||||||
|
|
||||||
### Log Atteso
|
|
||||||
|
|
||||||
```
|
|
||||||
[Kestrel] HTTPS disabled - running in HTTP-only mode
|
|
||||||
[Kestrel] Use a reverse proxy for SSL termination
|
|
||||||
[Kestrel] Listening on: http://+:8080
|
|
||||||
info: Microsoft.Hosting.Lifetime[14]
|
|
||||||
Now listening on: http://[::]:8080 ? CORRETTO!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Test della Correzione
|
|
||||||
|
|
||||||
### 1. Rebuild Container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build nuova immagine
|
|
||||||
docker build -t autobidder:latest .
|
|
||||||
|
|
||||||
# Verifica listening port nei log
|
|
||||||
docker run --rm autobidder:latest
|
|
||||||
|
|
||||||
# Output atteso:
|
|
||||||
# Now listening on: http://[::]:8080 ?
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Test con docker-compose
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose down
|
|
||||||
docker-compose build
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Verifica log
|
|
||||||
docker-compose logs -f autobidder
|
|
||||||
|
|
||||||
# Accedi a http://localhost:5000
|
|
||||||
# (host porta 5000 ? container porta 8080)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Test Manuale
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run container
|
|
||||||
docker run -d \
|
|
||||||
--name test-autobidder \
|
|
||||||
-p 5000:8080 \
|
|
||||||
autobidder:latest
|
|
||||||
|
|
||||||
# Verifica porta
|
|
||||||
docker port test-autobidder
|
|
||||||
# Output: 8080/tcp -> 0.0.0.0:5000 ?
|
|
||||||
|
|
||||||
# Test endpoint
|
|
||||||
curl http://localhost:5000
|
|
||||||
# Dovrebbe rispondere ?
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
docker stop test-autobidder
|
|
||||||
docker rm test-autobidder
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Port Mapping Corretto
|
|
||||||
|
|
||||||
### Docker Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Corretto: Host 5000 ? Container 8080
|
|
||||||
docker run -p 5000:8080 autobidder:latest
|
|
||||||
|
|
||||||
# Alternativa: Qualsiasi porta host
|
|
||||||
docker run -p 3000:8080 autobidder:latest # http://localhost:3000
|
|
||||||
docker run -p 8080:8080 autobidder:latest # http://localhost:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
autobidder:
|
|
||||||
ports:
|
|
||||||
- "5000:8080" # Host:Container ?
|
|
||||||
environment:
|
|
||||||
- ASPNETCORE_URLS=http://+:8080 # Conferma porta container
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unraid
|
|
||||||
|
|
||||||
```
|
|
||||||
Container Port: 8080
|
|
||||||
Host Port: 5000 (o qualsiasi altra porta disponibile)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Override Porta Container
|
|
||||||
|
|
||||||
Se vuoi cambiare la porta del container:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Opzione 1: Environment variable
|
|
||||||
docker run -p 5000:9000 \
|
|
||||||
-e ASPNETCORE_URLS=http://+:9000 \
|
|
||||||
autobidder:latest
|
|
||||||
|
|
||||||
# Opzione 2: Modifica Dockerfile
|
|
||||||
# ENV ASPNETCORE_URLS=http://+:9000
|
|
||||||
# EXPOSE 9000
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Troubleshooting
|
|
||||||
|
|
||||||
### Problema: Pagina ancora non carica
|
|
||||||
|
|
||||||
**Verifica porta container:**
|
|
||||||
```bash
|
|
||||||
docker ps
|
|
||||||
# PORTS: 0.0.0.0:5000->8080/tcp ?
|
|
||||||
|
|
||||||
# Verifica listening port dentro container
|
|
||||||
docker exec <container-id> netstat -tuln | grep LISTEN
|
|
||||||
# tcp6 0 0 :::8080 :::* LISTEN ?
|
|
||||||
```
|
|
||||||
|
|
||||||
**Verifica firewall:**
|
|
||||||
```bash
|
|
||||||
# Windows: Disabilita temporaneamente firewall
|
|
||||||
# Linux:
|
|
||||||
sudo ufw allow 5000/tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
**Verifica log applicazione:**
|
|
||||||
```bash
|
|
||||||
docker logs <container-id>
|
|
||||||
# Cerca errori dopo "Application started"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problema: Port already in use
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Trova processo su porta 5000
|
|
||||||
# Windows:
|
|
||||||
netstat -ano | findstr :5000
|
|
||||||
taskkill /PID <PID> /F
|
|
||||||
|
|
||||||
# Linux:
|
|
||||||
lsof -i :5000
|
|
||||||
kill <PID>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Checklist Fix Applicato
|
|
||||||
|
|
||||||
- [x] Rimossa configurazione esplicita HTTP in `Program.cs`
|
|
||||||
- [x] `ASPNETCORE_URLS` gestisce porta HTTP
|
|
||||||
- [x] Configurazione Kestrel solo per HTTPS opzionale
|
|
||||||
- [x] Log mostra porta corretta (8080)
|
|
||||||
- [x] Container accessibile da host
|
|
||||||
- [x] Build compila senza errori
|
|
||||||
- [x] Documentazione aggiornata
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Lezioni Apprese
|
|
||||||
|
|
||||||
1. **ASPNETCORE_URLS ha precedenza limitata**
|
|
||||||
- Configurazione esplicita Kestrel sovrascrive ASPNETCORE_URLS
|
|
||||||
- Meglio non configurare esplicitamente se usi variabili ambiente
|
|
||||||
|
|
||||||
2. **Separare HTTP da HTTPS**
|
|
||||||
- HTTP: gestito da ASPNETCORE_URLS
|
|
||||||
- HTTPS: configurato esplicitamente (se necessario)
|
|
||||||
|
|
||||||
3. **Verifica sempre i log**
|
|
||||||
- "Now listening on:" mostra la porta effettiva
|
|
||||||
- Ignora warning su port override se tutto funziona
|
|
||||||
|
|
||||||
4. **Port mapping deve corrispondere**
|
|
||||||
- Container port = porta in "Now listening on:"
|
|
||||||
- Host port = quello che usi nel browser
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**? FIX APPLICATO - Container ora ascolta correttamente sulla porta 8080!**
|
|
||||||
@@ -148,6 +148,175 @@ namespace AutoBidder.Models
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int PollingLatencyMs { get; set; } = 0; // Ultima latenza polling ms
|
public int PollingLatencyMs { get; set; } = 0; // Ultima latenza polling ms
|
||||||
|
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
// TRACKING AVANZATO PER STRATEGIE
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Storico latenze ultime N misurazioni (per media mobile)
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public List<int> LatencyHistory { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Numero massimo di latenze da memorizzare
|
||||||
|
/// </summary>
|
||||||
|
private const int MAX_LATENCY_HISTORY = 20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggiunge una misurazione di latenza allo storico
|
||||||
|
/// </summary>
|
||||||
|
public void AddLatencyMeasurement(int latencyMs)
|
||||||
|
{
|
||||||
|
LatencyHistory.Add(latencyMs);
|
||||||
|
if (LatencyHistory.Count > MAX_LATENCY_HISTORY)
|
||||||
|
LatencyHistory.RemoveAt(0);
|
||||||
|
PollingLatencyMs = latencyMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Latenza media calcolata sullo storico
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public double AverageLatencyMs => LatencyHistory.Count > 0
|
||||||
|
? LatencyHistory.Average()
|
||||||
|
: PollingLatencyMs > 0 ? PollingLatencyMs : 60;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Heat metric (0-100) che indica quanto è "calda" l'asta
|
||||||
|
/// Calcolato in base a: bidder attivi, frequenza puntate, collisioni
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public int HeatMetric { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Numero di bidder unici attivi negli ultimi N secondi
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public int ActiveBiddersCount { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Numero di collisioni rilevate (puntate nello stesso secondo)
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public int CollisionCount { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collisioni consecutive senza puntata vincente
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public int ConsecutiveCollisions { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp dell'ultimo soft retreat
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public DateTime? LastSoftRetreatAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Se true, l'asta è in soft retreat temporaneo
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool IsInSoftRetreat { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contatore puntate effettuate in questa sessione su questa asta
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public int SessionBidCount { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Numero di volte che il timer è scaduto prima della puntata
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public int TimerExpiredCount { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Numero di puntate riuscite
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public int SuccessfulBidCount { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Numero di puntate fallite
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public int FailedBidCount { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lista utenti identificati come aggressivi in questa asta
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public HashSet<string> AggressiveBidders { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offset dinamico calcolato per questa asta (ms)
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public int DynamicOffsetMs { get; set; } = 150;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offset effettivo usato nell'ultima puntata (include jitter)
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public int LastUsedOffsetMs { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indica se questa asta è stata seguita dall'inizio (per salvare storia completa)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsTrackedFromStart { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp di inizio tracking
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? TrackingStartedAt { get; set; }
|
||||||
|
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
// IMPOSTAZIONI PER-ASTA (override globali)
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override: abilita/disabilita strategie avanzate per questa asta
|
||||||
|
/// null = usa impostazione globale
|
||||||
|
/// </summary>
|
||||||
|
public bool? AdvancedStrategiesEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override: abilita/disabilita jitter per questa asta
|
||||||
|
/// </summary>
|
||||||
|
public bool? JitterEnabledOverride { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override: abilita/disabilita soft retreat per questa asta
|
||||||
|
/// </summary>
|
||||||
|
public bool? SoftRetreatEnabledOverride { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override: limite puntate per questa asta
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxBidsOverride { get; set; }
|
||||||
|
|
||||||
|
// ?? NUOVO: Rilevamento situazione di duello
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True se rilevata situazione di duello (solo 2 bidder dominanti)
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool IsDuelSituation { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Username dell'avversario in caso di duello
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public string? DuelOpponent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vantaggio/svantaggio nel duello (% puntate mie - % puntate avversario)
|
||||||
|
/// Positivo = sto dominando, Negativo = sto perdendo
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public double DuelAdvantage { get; set; } = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -117,4 +117,62 @@ namespace AutoBidder.Models
|
|||||||
|
|
||||||
public double WinRate => TotalAuctions > 0 ? (double)WonAuctions / TotalAuctions * 100 : 0;
|
public double WinRate => TotalAuctions > 0 ? (double)WonAuctions / TotalAuctions * 100 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Record completo storia asta con tutte le metriche avanzate
|
||||||
|
/// </summary>
|
||||||
|
public class CompleteAuctionHistoryRecord
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string AuctionId { get; set; } = "";
|
||||||
|
public string AuctionName { get; set; } = "";
|
||||||
|
public string? ProductKey { get; set; }
|
||||||
|
public string? OriginalUrl { get; set; }
|
||||||
|
|
||||||
|
// Dati finali
|
||||||
|
public double FinalPrice { get; set; }
|
||||||
|
public double? BuyNowPrice { get; set; }
|
||||||
|
public double? ShippingCost { get; set; }
|
||||||
|
public double? TotalCost { get; set; }
|
||||||
|
public double? Savings { get; set; }
|
||||||
|
public double? SavingsPercentage { get; set; }
|
||||||
|
|
||||||
|
// Risultato
|
||||||
|
public bool Won { get; set; }
|
||||||
|
public string? WinnerUsername { get; set; }
|
||||||
|
public int? WinnerBidsUsed { get; set; }
|
||||||
|
|
||||||
|
// Metriche competizione
|
||||||
|
public int TotalResets { get; set; }
|
||||||
|
public int TotalUniqueBidders { get; set; }
|
||||||
|
public int MaxHeatMetric { get; set; }
|
||||||
|
public double AvgHeatMetric { get; set; }
|
||||||
|
public int TotalCollisions { get; set; }
|
||||||
|
|
||||||
|
// Mie statistiche
|
||||||
|
public int MyBidsUsed { get; set; }
|
||||||
|
public int MySuccessfulBids { get; set; }
|
||||||
|
public int MyFailedBids { get; set; }
|
||||||
|
public int MyTimerExpired { get; set; }
|
||||||
|
public double? MyAvgLatencyMs { get; set; }
|
||||||
|
|
||||||
|
// Timestamps
|
||||||
|
public DateTime ClosedAt { get; set; }
|
||||||
|
public int ClosedAtHour { get; set; }
|
||||||
|
public int? DurationSeconds { get; set; }
|
||||||
|
public bool IsCompleteTracking { get; set; }
|
||||||
|
|
||||||
|
// JSON
|
||||||
|
public string? AggressiveBiddersJson { get; set; }
|
||||||
|
public string? BiddersSummaryJson { get; set; }
|
||||||
|
|
||||||
|
// Proprietà calcolate
|
||||||
|
public string DurationFormatted => DurationSeconds.HasValue
|
||||||
|
? TimeSpan.FromSeconds(DurationSeconds.Value).ToString(@"hh\:mm\:ss")
|
||||||
|
: "-";
|
||||||
|
|
||||||
|
public double SuccessRate => (MySuccessfulBids + MyFailedBids) > 0
|
||||||
|
? (double)MySuccessfulBids / (MySuccessfulBids + MyFailedBids) * 100
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,250 +0,0 @@
|
|||||||
# ?? Nuovo Workflow Docker + Gitea - RIEPILOGO
|
|
||||||
|
|
||||||
## ? Cosa è Cambiato
|
|
||||||
|
|
||||||
### PRIMA (Approccio Custom)
|
|
||||||
- Profili `.pubxml` con comandi Docker custom
|
|
||||||
- Non compatibili con GUI Visual Studio
|
|
||||||
- Richiedeva comandi manuali da terminale
|
|
||||||
|
|
||||||
### DOPO (Approccio Nativo Visual Studio)
|
|
||||||
- Profili `.pubxml` standard Docker di Visual Studio
|
|
||||||
- **Funziona dalla GUI** (Tasto destro ? Pubblica)
|
|
||||||
- Post-build target automatico nel `.csproj`
|
|
||||||
- Workflow completamente integrato
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Workflow Completo
|
|
||||||
|
|
||||||
```
|
|
||||||
???????????????????????????????????????????????????
|
|
||||||
? Visual Studio ? Tasto Destro ? Pubblica ?
|
|
||||||
? Seleziona profilo: GiteaRegistry ?
|
|
||||||
???????????????????????????????????????????????????
|
|
||||||
?
|
|
||||||
?
|
|
||||||
???????????????????????????????????????????????????
|
|
||||||
? 1. Build .NET (Release) ?
|
|
||||||
???????????????????????????????????????????????????
|
|
||||||
?
|
|
||||||
?
|
|
||||||
???????????????????????????????????????????????????
|
|
||||||
? 2. Docker build ?
|
|
||||||
? docker build -t autobidder:latest . ?
|
|
||||||
???????????????????????????????????????????????????
|
|
||||||
?
|
|
||||||
?
|
|
||||||
???????????????????????????????????????????????????
|
|
||||||
? 3. POST-BUILD TARGET (AutoBidder.csproj) ?
|
|
||||||
? - Tag: autobidder:latest ?
|
|
||||||
? ? gitea.../alby96/autobidder:latest ?
|
|
||||||
? - Tag: autobidder:latest ?
|
|
||||||
? ? gitea.../alby96/autobidder:1.0.0 ?
|
|
||||||
???????????????????????????????????????????????????
|
|
||||||
?
|
|
||||||
?
|
|
||||||
???????????????????????????????????????????????????
|
|
||||||
? 4. Push su Gitea ?
|
|
||||||
? - docker push .../autobidder:latest ?
|
|
||||||
? - docker push .../autobidder:1.0.0 ?
|
|
||||||
???????????????????????????????????????????????????
|
|
||||||
?
|
|
||||||
?
|
|
||||||
???????????????????????????????????????????????????
|
|
||||||
? ? PUBBLICATO SU GITEA ?
|
|
||||||
? https://gitea.../Alby96/-/packages ?
|
|
||||||
???????????????????????????????????????????????????
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Modificati
|
|
||||||
|
|
||||||
### 1. `AutoBidder.csproj`
|
|
||||||
|
|
||||||
**Aggiunto:**
|
|
||||||
```xml
|
|
||||||
<!-- POST-BUILD TARGET: Push automatico su Gitea -->
|
|
||||||
<Target Name="PushDockerImageToGitea" AfterTargets="Publish" Condition="'$(PushToGiteaRegistry)' == 'true'">
|
|
||||||
<!-- Tag e push automatico su gitea.encke-hake.ts.net/alby96/autobidder -->
|
|
||||||
</Target>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. `Properties/PublishProfiles/GiteaRegistry.pubxml` (NUOVO)
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<PropertyGroup>
|
|
||||||
<WebPublishMethod>Docker</WebPublishMethod>
|
|
||||||
<DockerPublish>true</DockerPublish>
|
|
||||||
<PublishProvider>DockerContainer</PublishProvider>
|
|
||||||
|
|
||||||
<DockerfileTag>autobidder:latest</DockerfileTag>
|
|
||||||
<PushToGiteaRegistry>true</PushToGiteaRegistry> <!-- Abilita push -->
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cosa fa:**
|
|
||||||
- Build Docker dell'immagine locale
|
|
||||||
- Attiva post-build target per push su Gitea
|
|
||||||
- **Funziona da GUI Visual Studio** ?
|
|
||||||
|
|
||||||
### 3. `Properties/PublishProfiles/GiteaRegistry-LocalOnly.pubxml` (NUOVO)
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<PropertyGroup>
|
|
||||||
<WebPublishMethod>Docker</WebPublishMethod>
|
|
||||||
<DockerPublish>true</DockerPublish>
|
|
||||||
<PublishProvider>DockerContainer</PublishProvider>
|
|
||||||
|
|
||||||
<DockerfileTag>autobidder:latest</DockerfileTag>
|
|
||||||
<PushToGiteaRegistry>false</PushToGiteaRegistry> <!-- NO push -->
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cosa fa:**
|
|
||||||
- Build Docker solo locale
|
|
||||||
- NESSUN push su Gitea
|
|
||||||
- Utile per test
|
|
||||||
|
|
||||||
### 4. `DOCKER_PUBLISH_GUIDE.md` (AGGIORNATA)
|
|
||||||
|
|
||||||
- Istruzioni per uso da Visual Studio GUI
|
|
||||||
- Workflow completo documentato
|
|
||||||
- Troubleshooting aggiornato
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Come Usare
|
|
||||||
|
|
||||||
### Opzione 1: Da Visual Studio (CONSIGLIATO)
|
|
||||||
|
|
||||||
1. **Tasto destro** sul progetto `AutoBidder`
|
|
||||||
2. Click **Pubblica**
|
|
||||||
3. Seleziona profilo: **`GiteaRegistry`**
|
|
||||||
4. Click **Pubblica**
|
|
||||||
|
|
||||||
? **FATTO!** L'immagine viene buildat?, taggata e pubblicata automaticamente.
|
|
||||||
|
|
||||||
### Opzione 2: Da Riga di Comando
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dotnet publish -c Release /p:PublishProfile=GiteaRegistry
|
|
||||||
```
|
|
||||||
|
|
||||||
### Opzione 3: Solo Build Locale (Test)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dotnet publish -c Release /p:PublishProfile=GiteaRegistry-LocalOnly
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Prerequisito: Autenticazione
|
|
||||||
|
|
||||||
**Prima volta (OBBLIGATORIO):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Genera Token PAT su Gitea
|
|
||||||
# https://gitea.encke-hake.ts.net/user/settings/applications
|
|
||||||
# Scope: read:packages + write:packages
|
|
||||||
|
|
||||||
# 2. Autentica Docker
|
|
||||||
docker login gitea.encke-hake.ts.net
|
|
||||||
# Username: Alby96
|
|
||||||
# Password: [TOKEN PAT]
|
|
||||||
```
|
|
||||||
|
|
||||||
**NOTA:** Se hai 2FA su Gitea, il Token PAT è **OBBLIGATORIO**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Vantaggi del Nuovo Approccio
|
|
||||||
|
|
||||||
| Aspetto | Prima (Custom) | Dopo (Nativo VS) |
|
|
||||||
|---------|----------------|------------------|
|
|
||||||
| **GUI Visual Studio** | ? Non funzionava | ? Funziona perfettamente |
|
|
||||||
| **Semplicità** | Comandi manuali | Click ? Pubblica |
|
|
||||||
| **Standard** | Approccio custom | Standard Microsoft |
|
|
||||||
| **Manutenibilità** | Complesso | Semplice |
|
|
||||||
| **Errori** | Difficili da debuggare | Output chiaro |
|
|
||||||
| **Workflow** | Multi-step manuale | Automatico end-to-end |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Verifica Post-Pubblicazione
|
|
||||||
|
|
||||||
Dopo la pubblicazione, Visual Studio mostrerà:
|
|
||||||
|
|
||||||
```
|
|
||||||
========================================
|
|
||||||
POST-BUILD: Tagging and pushing to Gitea Registry
|
|
||||||
========================================
|
|
||||||
Tagged: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
Tagged: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
========================================
|
|
||||||
Pushing to Gitea Registry...
|
|
||||||
========================================
|
|
||||||
Pushed: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
Pushed: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
========================================
|
|
||||||
SUCCESS: Images published to Gitea!
|
|
||||||
========================================
|
|
||||||
View on Gitea:
|
|
||||||
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder/latest
|
|
||||||
========================================
|
|
||||||
```
|
|
||||||
|
|
||||||
**Verifica su Gitea:**
|
|
||||||
- Vai su: `https://gitea.encke-hake.ts.net/Alby96/-/packages`
|
|
||||||
- Cerca package: `autobidder` (tipo: Container)
|
|
||||||
- Verifica tag: `latest` e `1.0.0`
|
|
||||||
- Controlla data: dovrebbe essere oggi
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Prossimi Passi
|
|
||||||
|
|
||||||
1. ? Autenticati con Docker (Token PAT)
|
|
||||||
2. ? Prova pubblicazione: Tasto destro ? Pubblica ? GiteaRegistry
|
|
||||||
3. ? Verifica su Gitea che l'immagine sia caricata
|
|
||||||
4. ? Deploy su Unraid/altro server
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Note Importanti
|
|
||||||
|
|
||||||
### Convenzione Nomi Gitea (CORRETTA)
|
|
||||||
|
|
||||||
```
|
|
||||||
gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
??????????????????????? ?????? ???????????
|
|
||||||
registro owner immagine
|
|
||||||
|
|
||||||
? 3 LIVELLI (corretto)
|
|
||||||
? Non usare: /alby96/mimante/autobidder (4 livelli - ERRATO)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Post-Build Condition
|
|
||||||
|
|
||||||
Il post-build target si attiva **SOLO** se:
|
|
||||||
- Profilo ha `<PushToGiteaRegistry>true</PushToGiteaRegistry>`
|
|
||||||
- `GiteaRegistry.pubxml` ? Push attivato ?
|
|
||||||
- `GiteaRegistry-LocalOnly.pubxml` ? Push disabilitato ?
|
|
||||||
|
|
||||||
### Aggiornamento Versione
|
|
||||||
|
|
||||||
Per pubblicare nuova versione:
|
|
||||||
1. Modifica `<Version>1.0.1</Version>` in `AutoBidder.csproj`
|
|
||||||
2. Pubblica normalmente
|
|
||||||
3. Vengono creati tag: `latest` (aggiornato) + `1.0.1` (nuovo)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**? CONFIGURAZIONE COMPLETATA!**
|
|
||||||
|
|
||||||
Ora hai un workflow professionale integrato con Visual Studio per pubblicare su Gitea! ??
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
# ?? Problema HTTPS in Docker - RISOLTO
|
|
||||||
|
|
||||||
## ? Errore Originale
|
|
||||||
|
|
||||||
```
|
|
||||||
Unhandled exception. System.InvalidOperationException:
|
|
||||||
Unable to configure HTTPS endpoint. No server certificate was specified,
|
|
||||||
and the default developer certificate could not be found or is out of date.
|
|
||||||
To generate a developer certificate run 'dotnet dev-certs https'.
|
|
||||||
To trust the certificate (Windows and macOS only) run 'dotnet dev-certs https --trust'.
|
|
||||||
|
|
||||||
at Program.<>c.<<Main>$>b__0_6(ListenOptions listenOptions) in /src/Program.cs:line 17
|
|
||||||
```
|
|
||||||
|
|
||||||
## ?? Analisi del Problema
|
|
||||||
|
|
||||||
### Causa
|
|
||||||
|
|
||||||
**Nel `Program.cs` (versione precedente):**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// PROBLEMA: In Development, enableHttps = true
|
|
||||||
var enableHttps = builder.Configuration.GetValue<bool>(
|
|
||||||
"Kestrel:EnableHttps",
|
|
||||||
builder.Environment.IsDevelopment() // ? true in Dev!
|
|
||||||
);
|
|
||||||
|
|
||||||
builder.WebHost.ConfigureKestrel(options =>
|
|
||||||
{
|
|
||||||
options.ListenAnyIP(5000); // HTTP
|
|
||||||
|
|
||||||
if (enableHttps)
|
|
||||||
{
|
|
||||||
options.ListenAnyIP(5001, listenOptions =>
|
|
||||||
{
|
|
||||||
// ? Cerca certificato che non esiste in container!
|
|
||||||
listenOptions.UseHttps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problema:**
|
|
||||||
- In ambiente `Development` (o assente), `enableHttps = true`
|
|
||||||
- In Docker, `ASPNETCORE_ENVIRONMENT=Production` ma il certificato non esiste
|
|
||||||
- Kestrel fallisce all'avvio cercando certificati di sviluppo
|
|
||||||
|
|
||||||
### Flusso Errore
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Docker build ? ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
2. Program.cs ? IsDevelopment() = false
|
|
||||||
3. Ma se Kestrel:EnableHttps non è settato ? usa default
|
|
||||||
4. In alcune configurazioni, tenta comunque HTTPS
|
|
||||||
5. listenOptions.UseHttps() ? cerca certificato
|
|
||||||
6. Certificato non trovato ? CRASH! ?
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Soluzione Implementata
|
|
||||||
|
|
||||||
### 1. Modifica `Program.cs`
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// ? CORRETTO: HTTPS disabilitato di default
|
|
||||||
var enableHttps = builder.Configuration.GetValue<bool>("Kestrel:EnableHttps", false);
|
|
||||||
|
|
||||||
builder.WebHost.ConfigureKestrel(options =>
|
|
||||||
{
|
|
||||||
options.ListenAnyIP(8080); // HTTP porta standard container
|
|
||||||
|
|
||||||
if (enableHttps)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Cerca certificato esplicito da configurazione
|
|
||||||
var certPath = builder.Configuration["Kestrel:Certificates:Default:Path"];
|
|
||||||
var certPassword = builder.Configuration["Kestrel:Certificates:Default:Password"];
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(certPath) && File.Exists(certPath))
|
|
||||||
{
|
|
||||||
// Usa certificato fornito (production con cert)
|
|
||||||
options.ListenAnyIP(8443, listenOptions =>
|
|
||||||
{
|
|
||||||
listenOptions.UseHttps(certPath, certPassword);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (builder.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
// Certificato dev SOLO se esplicitamente Development
|
|
||||||
options.ListenAnyIP(5001, listenOptions =>
|
|
||||||
{
|
|
||||||
listenOptions.UseHttps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine("[Kestrel] HTTPS requested but no certificate found");
|
|
||||||
Console.WriteLine("[Kestrel] Running in HTTP-only mode");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[Kestrel] Failed to enable HTTPS: {ex.Message}");
|
|
||||||
Console.WriteLine("[Kestrel] Running in HTTP-only mode");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine("[Kestrel] HTTPS disabled - running in HTTP-only mode");
|
|
||||||
Console.WriteLine("[Kestrel] Use a reverse proxy for SSL termination");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Modifica `Dockerfile`
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
# Environment variables
|
|
||||||
ENV ASPNETCORE_URLS=http://+:8080
|
|
||||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
ENV Kestrel__EnableHttps=false # ? Disabilita HTTPS esplicitamente
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Porta Cambiata
|
|
||||||
|
|
||||||
- ? Prima: `5000` (HTTP) + `5001` (HTTPS)
|
|
||||||
- ? Dopo: `8080` (HTTP standard container)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Confronto Prima/Dopo
|
|
||||||
|
|
||||||
| Aspetto | Prima | Dopo |
|
|
||||||
|---------|-------|------|
|
|
||||||
| **Default HTTPS** | ? Abilitato in Dev | ? Disabilitato |
|
|
||||||
| **Porta HTTP** | 5000 | 8080 (standard) |
|
|
||||||
| **Porta HTTPS** | 5001 (fallisce) | 8443 (opzionale) |
|
|
||||||
| **Certificato** | Richiesto | Opzionale |
|
|
||||||
| **Crash startup** | ? Sì | ? No |
|
|
||||||
| **Reverse proxy** | N/A | ? Consigliato |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Best Practices per HTTPS in Container
|
|
||||||
|
|
||||||
### ? NON FARE (Anti-pattern)
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
# ? SBAGLIATO: Abilita HTTPS senza certificato
|
|
||||||
ENV ASPNETCORE_URLS=https://+:5001
|
|
||||||
```
|
|
||||||
|
|
||||||
### ? PATTERN CORRETTO
|
|
||||||
|
|
||||||
**Opzione 1: HTTP Only + Reverse Proxy (CONSIGLIATO)**
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
# Container espone solo HTTP
|
|
||||||
ENV ASPNETCORE_URLS=http://+:8080
|
|
||||||
ENV Kestrel__EnableHttps=false
|
|
||||||
```
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
# Nginx gestisce SSL
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
|
||||||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://autobidder:8080;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Opzione 2: HTTPS con Certificato nel Container**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
-e Kestrel__EnableHttps=true \
|
|
||||||
-e Kestrel__Certificates__Default__Path=/certs/cert.pfx \
|
|
||||||
-e Kestrel__Certificates__Default__Password=password \
|
|
||||||
-v /host/certs:/certs \
|
|
||||||
-p 443:8443 \
|
|
||||||
autobidder:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Test Correzione
|
|
||||||
|
|
||||||
### Prima (ERRORE)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -p 5000:5000 autobidder:latest
|
|
||||||
# System.InvalidOperationException: Unable to configure HTTPS endpoint
|
|
||||||
# ? Container CRASH!
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dopo (SUCCESS)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -p 5000:8080 autobidder:latest
|
|
||||||
# [Kestrel] HTTPS disabled - running in HTTP-only mode
|
|
||||||
# [Kestrel] Use a reverse proxy for SSL termination
|
|
||||||
# ? Application started successfully!
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verifica
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Container in esecuzione
|
|
||||||
docker ps
|
|
||||||
# CONTAINER ID IMAGE PORTS
|
|
||||||
# abc123 autobidder:latest 0.0.0.0:5000->8080/tcp
|
|
||||||
|
|
||||||
# Test endpoint
|
|
||||||
curl http://localhost:5000
|
|
||||||
# ? Risposta OK!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Configurazione Unraid/Docker Compose
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
autobidder:
|
|
||||||
image: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
container_name: autobidder
|
|
||||||
ports:
|
|
||||||
- "5000:8080" # Host:Container
|
|
||||||
volumes:
|
|
||||||
- ./data:/app/Data
|
|
||||||
- ./logs:/app/logs
|
|
||||||
environment:
|
|
||||||
- ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
- Kestrel__EnableHttps=false
|
|
||||||
restart: unless-stopped
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unraid Template
|
|
||||||
|
|
||||||
```
|
|
||||||
Repository: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
Port: 5000 (host) ? 8080 (container) [HTTP]
|
|
||||||
Volume: /mnt/user/appdata/autobidder/data ? /app/Data
|
|
||||||
Volume: /mnt/user/appdata/autobidder/logs ? /app/logs
|
|
||||||
Environment: ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
Environment: Kestrel__EnableHttps=false
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Checklist Finale
|
|
||||||
|
|
||||||
- [x] HTTPS disabilitato di default in container
|
|
||||||
- [x] Porta HTTP cambiata da 5000 ? 8080 (standard)
|
|
||||||
- [x] Dockerfile aggiornato con `Kestrel__EnableHttps=false`
|
|
||||||
- [x] Program.cs modificato per gestire correttamente HTTPS opzionale
|
|
||||||
- [x] Certificati di sviluppo SOLO in ambiente Development
|
|
||||||
- [x] Reverse proxy consigliato per SSL in production
|
|
||||||
- [x] Documentazione aggiornata
|
|
||||||
- [x] Container si avvia senza errori
|
|
||||||
|
|
||||||
**PROBLEMA RISOLTO!** ??
|
|
||||||
|
|
||||||
Container ora si avvia correttamente in modalità HTTP-only, pronto per reverse proxy SSL in production.
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
# ?? PROBLEMA RISOLTO: Errore Visual Studio con Push Riuscito
|
|
||||||
|
|
||||||
## ?? Analisi del Problema
|
|
||||||
|
|
||||||
### ? Cosa Funzionava
|
|
||||||
|
|
||||||
Dal log di pubblicazione:
|
|
||||||
```
|
|
||||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
|
|
||||||
docker push gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
...
|
|
||||||
latest: digest: sha256:dc08591c525e29d881f65effbc569a1c4c75d7d43614d75231e9c8035e3865b0 size: 856
|
|
||||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
|
|
||||||
docker push gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
...
|
|
||||||
1.0.0: digest: sha256:dc08591c525e29d881f65effbc569a1c4c75d7d43614d75231e9c8035e3865b0 size: 856
|
|
||||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
? ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ?
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
```
|
|
||||||
|
|
||||||
**Tutto perfetto**: Build, tag e push su Gitea funzionanti al 100%!
|
|
||||||
|
|
||||||
### ? Errore Visual Studio
|
|
||||||
|
|
||||||
Alla fine del processo:
|
|
||||||
```
|
|
||||||
1>La compilazione non è riuscita. Vedere la finestra di output per altre informazioni.
|
|
||||||
========== Pubblicazione: 0 completato/i, 1 non riuscito/i, 0 ignorato/i ==========
|
|
||||||
|
|
||||||
Errore MSB4057: la destinazione "ContainerBuild" non è presente nel progetto.
|
|
||||||
C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Sdks\Microsoft.Docker.Sdk\build\Microsoft.Docker.targets(173,5)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Causa del Problema
|
|
||||||
|
|
||||||
### Configurazione Precedente (ERRATA)
|
|
||||||
|
|
||||||
**File:** `Properties/PublishProfiles/GiteaRegistry.pubxml`
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<PropertyGroup>
|
|
||||||
<!-- ? PROBLEMA: Usa Docker SDK di Visual Studio -->
|
|
||||||
<WebPublishMethod>Docker</WebPublishMethod>
|
|
||||||
<DockerPublish>true</DockerPublish>
|
|
||||||
<PublishProvider>DockerContainer</PublishProvider>
|
|
||||||
<_TargetId>Docker</_TargetId>
|
|
||||||
|
|
||||||
<DockerfileTag>autobidder:latest</DockerfileTag>
|
|
||||||
<PushToGiteaRegistry>true</PushToGiteaRegistry>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problema:**
|
|
||||||
- `<WebPublishMethod>Docker</WebPublishMethod>` richiede **Microsoft.Docker.Sdk**
|
|
||||||
- Visual Studio cerca il target `ContainerBuild` nel progetto
|
|
||||||
- Il target non esiste perché l'SDK non è installato (e non serve!)
|
|
||||||
- Visual Studio fallisce DOPO che il nostro workflow custom ha già pubblicato con successo
|
|
||||||
|
|
||||||
### Flusso Esecuzione
|
|
||||||
|
|
||||||
```
|
|
||||||
1. ? Build .NET (Release)
|
|
||||||
2. ? Publish files ? obj\Docker\publish
|
|
||||||
3. ? Post-build target "PushDockerImageToGitea" (dal .csproj)
|
|
||||||
?? ? docker build
|
|
||||||
?? ? docker tag
|
|
||||||
?? ? docker push (SUCCESSO!)
|
|
||||||
4. ? Visual Studio cerca target "ContainerBuild" (Docker SDK)
|
|
||||||
5. ? Target non trovato ? ERRORE (ma push già fatto!)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Soluzione Implementata
|
|
||||||
|
|
||||||
### Nuova Configurazione (CORRETTA)
|
|
||||||
|
|
||||||
**File:** `Properties/PublishProfiles/GiteaRegistry.pubxml`
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<PropertyGroup>
|
|
||||||
<!-- ? CORRETTO: Usa Custom senza Docker SDK -->
|
|
||||||
<WebPublishMethod>Custom</WebPublishMethod>
|
|
||||||
<PublishProvider>FileSystem</PublishProvider>
|
|
||||||
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
|
||||||
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
|
||||||
|
|
||||||
<!-- Path pubblicazione temporanea -->
|
|
||||||
<PublishUrl>obj\Docker\publish</PublishUrl>
|
|
||||||
<DeleteExistingFiles>True</DeleteExistingFiles>
|
|
||||||
|
|
||||||
<!-- Configurazione Docker -->
|
|
||||||
<DockerfileTag>autobidder:latest</DockerfileTag>
|
|
||||||
<DockerfilePath>$(MSBuildProjectDirectory)\Dockerfile</DockerfilePath>
|
|
||||||
<DockerfileContext>$(MSBuildProjectDirectory)</DockerfileContext>
|
|
||||||
|
|
||||||
<!-- Abilita post-build Gitea -->
|
|
||||||
<PushToGiteaRegistry>true</PushToGiteaRegistry>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<!-- Target Docker Build custom -->
|
|
||||||
<Target Name="DockerBuild" AfterTargets="GatherAllFilesToPublish">
|
|
||||||
<Message Importance="high" Text="?? Building Docker image..." />
|
|
||||||
<Exec Command="docker build -t $(DockerfileTag) -f "$(DockerfilePath)" "$(DockerfileContext)"" />
|
|
||||||
<Message Importance="high" Text="? Docker build completed!" />
|
|
||||||
</Target>
|
|
||||||
</Project>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Vantaggi:**
|
|
||||||
- ? Non richiede Microsoft.Docker.Sdk
|
|
||||||
- ? Visual Studio non cerca target mancanti
|
|
||||||
- ? Controllo completo del workflow
|
|
||||||
- ? Nessun errore alla fine del processo
|
|
||||||
|
|
||||||
### Nuovo Flusso Esecuzione
|
|
||||||
|
|
||||||
```
|
|
||||||
1. ? Build .NET (Release)
|
|
||||||
2. ? Publish files ? obj\Docker\publish
|
|
||||||
3. ? Target "DockerBuild" (dal profilo .pubxml)
|
|
||||||
?? docker build -t autobidder:latest
|
|
||||||
4. ? Post-build target "PushDockerImageToGitea" (dal .csproj)
|
|
||||||
?? docker tag ? gitea.../autobidder:latest
|
|
||||||
?? docker tag ? gitea.../autobidder:1.0.0
|
|
||||||
?? docker push latest
|
|
||||||
?? docker push 1.0.0
|
|
||||||
5. ? Visual Studio: SUCCESS! ?
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Confronto Prima/Dopo
|
|
||||||
|
|
||||||
| Aspetto | Prima (Docker SDK) | Dopo (Custom) |
|
|
||||||
|---------|-------------------|---------------|
|
|
||||||
| **WebPublishMethod** | `Docker` | `Custom` |
|
|
||||||
| **Richiede SDK** | ? Sì (non installato) | ? No |
|
|
||||||
| **Docker Build** | Post-build .csproj | Target .pubxml + Post-build |
|
|
||||||
| **Errore finale** | ? Sì (target mancante) | ? No |
|
|
||||||
| **Push funziona** | ? Sì | ? Sì |
|
|
||||||
| **Visual Studio OK** | ? No (errore) | ? Sì |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Risultato Finale
|
|
||||||
|
|
||||||
### Output Pubblicazione Corretta
|
|
||||||
|
|
||||||
```
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
? DOCKER BUILD: Building container image ?
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
|
|
||||||
?? Building: autobidder:latest
|
|
||||||
? Docker build completed successfully!
|
|
||||||
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
? POST-BUILD: Pubblicazione su Gitea Container Registry ?
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
|
|
||||||
?? Solution Version: 1.0.0
|
|
||||||
??? Target Tags:
|
|
||||||
• gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
• gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
|
|
||||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
? ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ?
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
|
|
||||||
========== Pubblicazione: 1 completato/i, 0 non riuscito/i, 0 ignorato/i ==========
|
|
||||||
```
|
|
||||||
|
|
||||||
**Visual Studio mostra SUCCESS senza errori!** ?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Lezioni Apprese
|
|
||||||
|
|
||||||
1. **`WebPublishMethod=Docker`** richiede Microsoft.Docker.Sdk installato
|
|
||||||
2. **`WebPublishMethod=Custom`** permette workflow personalizzati senza SDK
|
|
||||||
3. Il nostro workflow custom funzionava già (push riuscito), ma Visual Studio non era soddisfatto
|
|
||||||
4. Separare build Docker (target nel .pubxml) da push Gitea (target nel .csproj) rende il processo più chiaro
|
|
||||||
5. Visual Studio può mostrare errori anche se l'operazione è riuscita (cerca target che non trova)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Checklist Verifica
|
|
||||||
|
|
||||||
- [x] Build .NET funziona
|
|
||||||
- [x] Docker build funziona
|
|
||||||
- [x] Tag Gitea creati
|
|
||||||
- [x] Push su Gitea riuscito
|
|
||||||
- [x] Visual Studio non mostra errori
|
|
||||||
- [x] Digest SHA256 visibile su Gitea
|
|
||||||
- [x] Immagini disponibili per pull
|
|
||||||
|
|
||||||
**TUTTO FUNZIONANTE!** ??
|
|
||||||
+24
-20
@@ -48,13 +48,13 @@
|
|||||||
|
|
||||||
<!-- Pulsanti Azioni (Centro-Destra) -->
|
<!-- Pulsanti Azioni (Centro-Destra) -->
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<button class="btn btn-success hover-lift" @onclick="StartAll" disabled="@isMonitoringActive">
|
<button class="btn btn-success hover-lift" @onclick="StartAll">
|
||||||
<i class="bi bi-play-fill"></i> Avvia Tutto
|
<i class="bi bi-play-fill"></i> Avvia Tutto
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-warning hover-lift" @onclick="PauseAll" disabled="@(!isMonitoringActive)">
|
<button class="btn btn-warning hover-lift" @onclick="PauseAll">
|
||||||
<i class="bi bi-pause-fill"></i> Pausa Tutto
|
<i class="bi bi-pause-fill"></i> Pausa Tutto
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger hover-lift" @onclick="StopAll" disabled="@(!isMonitoringActive)">
|
<button class="btn btn-danger hover-lift" @onclick="StopAll">
|
||||||
<i class="bi bi-stop-fill"></i> Ferma Tutto
|
<i class="bi bi-stop-fill"></i> Ferma Tutto
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary ms-3 hover-lift" @onclick="ShowAddAuctionDialog">
|
<button class="btn btn-primary ms-3 hover-lift" @onclick="ShowAddAuctionDialog">
|
||||||
@@ -63,6 +63,9 @@
|
|||||||
<button class="btn btn-secondary hover-lift" @onclick="RemoveSelectedAuction" disabled="@(selectedAuction == null)">
|
<button class="btn btn-secondary hover-lift" @onclick="RemoveSelectedAuction" disabled="@(selectedAuction == null)">
|
||||||
<i class="bi bi-trash"></i> Rimuovi
|
<i class="bi bi-trash"></i> Rimuovi
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-outline-danger hover-lift" @onclick="RemoveAllAuctions" disabled="@(auctions.Count == 0)" title="Rimuovi tutte le aste (quelle terminate verranno salvate)">
|
||||||
|
<i class="bi bi-trash-fill"></i> Rimuovi Tutte
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -166,7 +169,7 @@
|
|||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="log-box">
|
<div class="log-box" id="globalLogContainer" @ref="globalLogRef">
|
||||||
@if (globalLog.Count == 0)
|
@if (globalLog.Count == 0)
|
||||||
{
|
{
|
||||||
<div class="text-muted"><i class="bi bi-inbox"></i> Nessun log ancora...</div>
|
<div class="text-muted"><i class="bi bi-inbox"></i> Nessun log ancora...</div>
|
||||||
@@ -178,6 +181,7 @@
|
|||||||
<div class="@GetLogEntryClass(logEntry)">@logEntry.Message</div>
|
<div class="@GetLogEntryClass(logEntry)">@logEntry.Message</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
<div id="logScrollAnchor"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -255,21 +259,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 info-group">
|
<div class="col-md-12 info-group">
|
||||||
<label><i class="bi bi-arrow-repeat"></i> Min Reset:</label>
|
<label><i class="bi bi-hand-index-thumb"></i> Max Puntate (0 = illimitate):</label>
|
||||||
<input type="number" class="form-control" @bind="selectedAuction.MinResets" @bind:after="SaveAuctions" />
|
<input type="number" class="form-control" @bind="selectedAuction.MaxBidsOverride" @bind:after="SaveAuctions" />
|
||||||
|
<small class="text-muted">Limite puntate per questa asta. 0 o vuoto = usa limite globale.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 info-group">
|
|
||||||
<label><i class="bi bi-arrow-repeat"></i> Max Reset:</label>
|
|
||||||
<input type="number" class="form-control" @bind="selectedAuction.MaxResets" @bind:after="SaveAuctions" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-check mt-2">
|
|
||||||
<input type="checkbox" class="form-check-input" id="checkOpen" @bind="selectedAuction.CheckAuctionOpenBeforeBid" @bind:after="SaveAuctions" />
|
|
||||||
<label class="form-check-label" for="checkOpen">
|
|
||||||
Verifica asta aperta prima di puntare
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pulsante Applica Limiti Consigliati -->
|
<!-- Pulsante Applica Limiti Consigliati -->
|
||||||
@@ -442,17 +436,27 @@
|
|||||||
@{
|
@{
|
||||||
// Crea una copia thread-safe per evitare modifiche durante l'enumerazione
|
// Crea una copia thread-safe per evitare modifiche durante l'enumerazione
|
||||||
var recentBidsCopy = GetRecentBidsSafe(selectedAuction);
|
var recentBidsCopy = GetRecentBidsSafe(selectedAuction);
|
||||||
|
|
||||||
|
// ?? FIX: Per l'utente corrente, usa BidsUsedOnThisAuction (valore ufficiale dal server)
|
||||||
|
var myOfficialBidsCount = selectedAuction.BidsUsedOnThisAuction ?? 0;
|
||||||
|
var currentUsername = GetCurrentUsername();
|
||||||
}
|
}
|
||||||
@if (recentBidsCopy.Any())
|
@if (recentBidsCopy.Any())
|
||||||
{
|
{
|
||||||
// Calcola statistiche puntatori
|
// Calcola statistiche puntatori
|
||||||
var bidderStats = recentBidsCopy
|
var bidderStats = recentBidsCopy
|
||||||
.GroupBy(b => b.Username)
|
.GroupBy(b => b.Username)
|
||||||
.Select(g => new { Username = g.Key, Count = g.Count(), IsMe = g.First().IsMyBid })
|
.Select(g => new {
|
||||||
|
Username = g.Key,
|
||||||
|
// Per l'utente corrente usa il conteggio ufficiale, per gli altri conta dalla lista
|
||||||
|
Count = g.First().IsMyBid && myOfficialBidsCount > 0 ? myOfficialBidsCount : g.Count(),
|
||||||
|
IsMe = g.First().IsMyBid
|
||||||
|
})
|
||||||
.OrderByDescending(s => s.Count)
|
.OrderByDescending(s => s.Count)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var totalBids = recentBidsCopy.Count;
|
// Ricalcola il totale includendo il conteggio corretto per l'utente
|
||||||
|
var totalBids = bidderStats.Sum(b => b.Count);
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-striped">
|
<table class="table table-sm table-striped">
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ namespace AutoBidder.Pages
|
|||||||
private string? recommendationMessage = null;
|
private string? recommendationMessage = null;
|
||||||
private bool recommendationSuccess = false;
|
private bool recommendationSuccess = false;
|
||||||
|
|
||||||
|
// Auto-scroll log
|
||||||
|
private ElementReference globalLogRef;
|
||||||
|
private int lastLogCount = 0;
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
// Sottoscrivi agli eventi del servizio di stato (ASYNC)
|
// Sottoscrivi agli eventi del servizio di stato (ASYNC)
|
||||||
@@ -91,6 +95,17 @@ namespace AutoBidder.Pages
|
|||||||
await JSRuntime.InvokeVoidAsync("addDeleteKeyListener",
|
await JSRuntime.InvokeVoidAsync("addDeleteKeyListener",
|
||||||
DotNetObjectReference.Create(this));
|
DotNetObjectReference.Create(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-scroll log globale quando ci sono nuovi messaggi
|
||||||
|
if (globalLog.Count != lastLogCount)
|
||||||
|
{
|
||||||
|
lastLogCount = globalLog.Count;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("scrollToBottom", "globalLogContainer");
|
||||||
|
}
|
||||||
|
catch { /* Ignora errori JS */ }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler async per eventi da background thread
|
// Handler async per eventi da background thread
|
||||||
@@ -462,6 +477,12 @@ namespace AutoBidder.Pages
|
|||||||
// Decodifica HTML entities
|
// Decodifica HTML entities
|
||||||
productName = System.Net.WebUtility.HtmlDecode(productName);
|
productName = System.Net.WebUtility.HtmlDecode(productName);
|
||||||
|
|
||||||
|
// ?? FIX: Sostituisci entità HTML non standard
|
||||||
|
productName = productName
|
||||||
|
.Replace("+", "+")
|
||||||
|
.Replace("&plus;", "+")
|
||||||
|
.Replace(" + ", " & "); // Normalizza separatori
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(productName) && productName != auction.Name)
|
if (!string.IsNullOrWhiteSpace(productName) && productName != auction.Name)
|
||||||
{
|
{
|
||||||
auction.Name = productName;
|
auction.Name = productName;
|
||||||
@@ -562,6 +583,42 @@ namespace AutoBidder.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task RemoveAllAuctions()
|
||||||
|
{
|
||||||
|
if (auctions.Count == 0) return;
|
||||||
|
|
||||||
|
var count = auctions.Count;
|
||||||
|
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm",
|
||||||
|
$"Rimuovere TUTTE le {count} aste?\n\n" +
|
||||||
|
"?? Le aste terminate verranno salvate automaticamente nelle statistiche.\n" +
|
||||||
|
"Le aste non terminate andranno perse.");
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Copia la lista per iterare in modo sicuro
|
||||||
|
var auctionsToRemove = auctions.ToList();
|
||||||
|
|
||||||
|
foreach (var auction in auctionsToRemove)
|
||||||
|
{
|
||||||
|
AuctionMonitor.RemoveAuction(auction.AuctionId);
|
||||||
|
AppState.RemoveAuction(auction);
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveAuctions();
|
||||||
|
selectedAuction = null;
|
||||||
|
|
||||||
|
AddLog($"[BULK] Rimosse {count} aste");
|
||||||
|
await JSRuntime.InvokeVoidAsync("alert", $"? Rimosse {count} aste con successo");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AddLog($"Errore rimozione bulk: {ex.Message}");
|
||||||
|
await JSRuntime.InvokeVoidAsync("alert", $"Errore durante la rimozione:\n{ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task RemoveSelectedAuctionWithConfirm()
|
private async Task RemoveSelectedAuctionWithConfirm()
|
||||||
{
|
{
|
||||||
if (selectedAuction == null) return;
|
if (selectedAuction == null) return;
|
||||||
@@ -711,7 +768,6 @@ namespace AutoBidder.Pages
|
|||||||
// Stati controllati dall'utente
|
// Stati controllati dall'utente
|
||||||
if (!auction.IsActive) return "Fermata";
|
if (!auction.IsActive) return "Fermata";
|
||||||
if (auction.IsPaused) return "Pausa";
|
if (auction.IsPaused) return "Pausa";
|
||||||
if (auction.IsAttackInProgress) return "Puntando";
|
|
||||||
return "Attiva";
|
return "Attiva";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,7 +797,6 @@ namespace AutoBidder.Pages
|
|||||||
// Stati controllati dall'utente
|
// Stati controllati dall'utente
|
||||||
if (!auction.IsActive) return "<i class='bi bi-stop-circle'></i>";
|
if (!auction.IsActive) return "<i class='bi bi-stop-circle'></i>";
|
||||||
if (auction.IsPaused) return "<i class='bi bi-pause-circle'></i>";
|
if (auction.IsPaused) return "<i class='bi bi-pause-circle'></i>";
|
||||||
if (auction.IsAttackInProgress) return "<i class='bi bi-lightning-charge-fill'></i>";
|
|
||||||
return "<i class='bi bi-play-circle-fill'></i>";
|
return "<i class='bi bi-play-circle-fill'></i>";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -849,6 +904,11 @@ namespace AutoBidder.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetCurrentUsername()
|
||||||
|
{
|
||||||
|
return sessionUsername ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
// ?? NUOVI METODI: Visualizzazione valori prodotto
|
// ?? NUOVI METODI: Visualizzazione valori prodotto
|
||||||
|
|
||||||
private string GetTotalCostDisplay(AuctionInfo? auction)
|
private string GetTotalCostDisplay(AuctionInfo? auction)
|
||||||
|
|||||||
@@ -163,12 +163,6 @@
|
|||||||
<label class="form-label fw-bold"><i class="bi bi-shield-check"></i> Puntate minime da mantenere</label>
|
<label class="form-label fw-bold"><i class="bi bi-shield-check"></i> Puntate minime da mantenere</label>
|
||||||
<input type="number" class="form-control" @bind="settings.MinimumRemainingBids" />
|
<input type="number" class="form-control" @bind="settings.MinimumRemainingBids" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="checkAuction" @bind="settings.DefaultCheckAuctionOpenBeforeBid" />
|
|
||||||
<label class="form-check-label" for="checkAuction">Verifica asta aperta prima di puntare</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
@@ -178,6 +172,255 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- STRATEGIE AVANZATE -->
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="heading-strategies">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-strategies" aria-expanded="false" aria-controls="collapse-strategies">
|
||||||
|
<i class="bi bi-lightning-charge me-2"></i> Strategie Avanzate
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapse-strategies" class="accordion-collapse collapse" aria-labelledby="heading-strategies" data-bs-parent="#settingsAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<h6 class="fw-bold mb-3"><i class="bi bi-speedometer2"></i> Timing & Latenza</h6>
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" class="form-check-input" id="adaptiveLatency" @bind="settings.AdaptiveLatencyEnabled" />
|
||||||
|
<label class="form-check-label" for="adaptiveLatency">
|
||||||
|
<strong>Compensazione latenza adattiva</strong>
|
||||||
|
<div class="form-text">Misura e compensa automaticamente la latenza di rete</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" class="form-check-input" id="jitter" @bind="settings.JitterEnabled" />
|
||||||
|
<label class="form-check-label" for="jitter">
|
||||||
|
<strong>Jitter casuale</strong>
|
||||||
|
<div class="form-text">Aggiunge variazione random per evitare sincronizzazione con altri bot</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label class="form-label"><i class="bi bi-shuffle"></i> Range jitter (±ms)</label>
|
||||||
|
<input type="number" class="form-control" @bind="settings.JitterRangeMs" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" class="form-check-input" id="dynamicOffset" @bind="settings.DynamicOffsetEnabled" />
|
||||||
|
<label class="form-check-label" for="dynamicOffset">
|
||||||
|
<strong>Offset dinamico</strong>
|
||||||
|
<div class="form-text">Adatta l'anticipo in base a heat, collisioni e volatilità</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label class="form-label">Offset minimo (ms)</label>
|
||||||
|
<input type="number" class="form-control" @bind="settings.MinimumOffsetMs" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label class="form-label">Offset massimo (ms)</label>
|
||||||
|
<input type="number" class="form-control" @bind="settings.MaximumOffsetMs" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 class="fw-bold mb-3"><i class="bi bi-robot"></i> Strategia Anti-AutoBid Bidoo</h6>
|
||||||
|
<div class="alert alert-info mb-3">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
<strong>Come funziona:</strong> Bidoo ha un sistema di auto-puntata integrato che si attiva a ~2 secondi.
|
||||||
|
Aspettando che il timer scenda sotto questa soglia, lasciamo che gli utenti con auto-puntata attiva
|
||||||
|
puntino prima di noi, <strong>risparmiando puntate</strong>.
|
||||||
|
</div>
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" class="form-check-input" id="waitAutoBid" @bind="settings.WaitForAutoBidEnabled" />
|
||||||
|
<label class="form-check-label" for="waitAutoBid">
|
||||||
|
<strong>Attendi auto-puntate Bidoo</strong>
|
||||||
|
<div class="form-text">Aspetta che il timer scenda sotto la soglia prima di puntare (consigliato)</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label class="form-label">Soglia attesa (secondi)</label>
|
||||||
|
<input type="number" step="0.1" class="form-control" @bind="settings.WaitForAutoBidThresholdSeconds" />
|
||||||
|
<div class="form-text">Punta solo quando timer < questo valore (default: 1.8s)</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<div class="form-check form-switch mt-4">
|
||||||
|
<input type="checkbox" class="form-check-input" id="logAutoBid" @bind="settings.LogAutoBidWaitSkips" />
|
||||||
|
<label class="form-check-label" for="logAutoBid">
|
||||||
|
<strong>Log attese</strong>
|
||||||
|
<div class="form-text">Logga quando salta per aspettare (verbose)</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 class="fw-bold mb-3"><i class="bi bi-thermometer-half"></i> Rilevamento Competizione</h6>
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" class="form-check-input" id="competition" @bind="settings.CompetitionDetectionEnabled" />
|
||||||
|
<label class="form-check-label" for="competition">
|
||||||
|
<strong>Rilevamento competizione</strong>
|
||||||
|
<div class="form-text">Monitora bidder attivi e calcola "heat metric"</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label class="form-label">Finestra (secondi)</label>
|
||||||
|
<input type="number" class="form-control" @bind="settings.CompetitionWindowSeconds" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label class="form-label">Soglia bidder attivi</label>
|
||||||
|
<input type="number" class="form-control" @bind="settings.CompetitionThreshold" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" class="form-check-input" id="autoPause" @bind="settings.AutoPauseHotAuctions" />
|
||||||
|
<label class="form-check-label" for="autoPause">
|
||||||
|
<strong>Auto-pausa aste calde</strong>
|
||||||
|
<div class="form-text">Pausa automatica se heat > soglia</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 class="fw-bold mb-3"><i class="bi bi-arrow-left-right"></i> Soft Retreat</h6>
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" class="form-check-input" id="softRetreat" @bind="settings.SoftRetreatEnabled" />
|
||||||
|
<label class="form-check-label" for="softRetreat">
|
||||||
|
<strong>Soft retreat automatico</strong>
|
||||||
|
<div class="form-text">Pausa temporanea dopo N collisioni consecutive</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label class="form-label">Collisioni per attivazione</label>
|
||||||
|
<input type="number" class="form-control" @bind="settings.SoftRetreatAfterCollisions" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label class="form-label">Durata pausa (secondi)</label>
|
||||||
|
<input type="number" class="form-control" @bind="settings.SoftRetreatDurationSeconds" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 class="fw-bold mb-3"><i class="bi bi-dice-5"></i> Puntata Probabilistica</h6>
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" class="form-check-input" id="probabilistic" @bind="settings.ProbabilisticBiddingEnabled" />
|
||||||
|
<label class="form-check-label" for="probabilistic">
|
||||||
|
<strong>Policy probabilistica</strong>
|
||||||
|
<div class="form-text">Decide se puntare con probabilità basata su competizione</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label class="form-label">Probabilità base (0-1)</label>
|
||||||
|
<input type="number" step="0.1" class="form-control" @bind="settings.BaseBidProbability" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<h6 class="fw-bold mb-3"><i class="bi bi-person-badge"></i> Profiling Avversari</h6>
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" class="form-check-input" id="profiling" @bind="settings.OpponentProfilingEnabled" />
|
||||||
|
<label class="form-check-label" for="profiling">
|
||||||
|
<strong>Profiling avversari</strong>
|
||||||
|
<div class="form-text">Identifica utenti aggressivi e rileva situazioni di "duello" tra 2 bidder. L'utente corrente NON viene mai considerato aggressivo.</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label class="form-label">Soglia puntate aggressivo</label>
|
||||||
|
<input type="number" class="form-control" @bind="settings.AggressiveBidderThreshold" />
|
||||||
|
<div class="form-text">Puntate minime per essere considerato aggressivo</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label class="form-label">Finestra analisi (puntate)</label>
|
||||||
|
<input type="number" class="form-control" @bind="settings.AggressiveBidderWindowSize" />
|
||||||
|
<div class="form-text">Analizza le ultime N puntate (default: 30)</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label class="form-label">Soglia % aggressivo</label>
|
||||||
|
<input type="number" step="5" class="form-control" @bind="settings.AggressiveBidderPercentageThreshold" />
|
||||||
|
<div class="form-text">% puntate nella finestra per essere aggressivo</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label class="form-label">Finestra rilevamento duello</label>
|
||||||
|
<input type="number" class="form-control" @bind="settings.DuelDetectionWindowSize" />
|
||||||
|
<div class="form-text">Puntate da analizzare per rilevare duelli (2 bidder dominanti)</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label class="form-label">Azione bidder aggressivi</label>
|
||||||
|
<select class="form-select" @bind="settings.AggressiveBidderAction">
|
||||||
|
<option value="Compete">? Continua normalmente (consigliato)</option>
|
||||||
|
<option value="Avoid">?? Evita asta (pausa automatica)</option>
|
||||||
|
<option value="Outbid">?? Punta più aggressivamente</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 class="fw-bold mb-3"><i class="bi bi-wallet2"></i> Gestione Budget</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" class="form-check-input" id="bankroll" @bind="settings.BankrollManagerEnabled" />
|
||||||
|
<label class="form-check-label" for="bankroll">
|
||||||
|
<strong>Bankroll manager</strong>
|
||||||
|
<div class="form-text">Limita puntate per sessione/asta/budget</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label class="form-label">Max puntate/sessione</label>
|
||||||
|
<input type="number" class="form-control" @bind="settings.MaxBidsPerSession" />
|
||||||
|
<div class="form-text">0 = illimitato</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label class="form-label">Max puntate/asta</label>
|
||||||
|
<input type="number" class="form-control" @bind="settings.MaxBidsPerAuction" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label class="form-label">Budget giornaliero (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" @bind="settings.DailyBudgetEuro" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label class="form-label">Costo medio puntata (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" @bind="settings.AverageBidCostEuro" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 d-flex gap-2 flex-wrap">
|
||||||
|
<button class="btn btn-success" @onclick="SaveSettings">
|
||||||
|
<i class="bi bi-check-lg"></i> Salva Strategie
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" @onclick="ApplyStrategiesToAllAuctions" disabled="@isApplyingToAll">
|
||||||
|
@if (isApplyingToAll)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||||
|
}
|
||||||
|
<i class="bi bi-arrow-repeat"></i> Applica a tutte le aste
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(applyToAllMessage))
|
||||||
|
{
|
||||||
|
<div class="alert @(applyToAllSuccess ? "alert-success" : "alert-warning") mt-3 mb-0">
|
||||||
|
<i class="bi @(applyToAllSuccess ? "bi-check-circle" : "bi-exclamation-triangle")"></i>
|
||||||
|
@applyToAllMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- LIMITI LOG -->
|
<!-- LIMITI LOG -->
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header" id="heading-logs">
|
<h2 class="accordion-header" id="heading-logs">
|
||||||
@@ -334,7 +577,7 @@
|
|||||||
<h6 class="text-muted mb-2">Versione</h6>
|
<h6 class="text-muted mb-2">Versione</h6>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<i class="bi bi-box-seam text-primary me-2" style="font-size: 1.5rem;"></i>
|
<i class="bi bi-box-seam text-primary me-2" style="font-size: 1.5rem;"></i>
|
||||||
<span class="fs-4 fw-bold">v1.0.0</span>
|
<span class="fs-4 fw-bold">v1.3.0</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -418,6 +661,11 @@ private string usernameInput = "";
|
|||||||
private string? connectionError;
|
private string? connectionError;
|
||||||
private bool isConnecting;
|
private bool isConnecting;
|
||||||
|
|
||||||
|
// Applica a tutte le aste
|
||||||
|
private bool isApplyingToAll = false;
|
||||||
|
private string? applyToAllMessage = null;
|
||||||
|
private bool applyToAllSuccess = false;
|
||||||
|
|
||||||
private AutoBidder.Utilities.AppSettings settings = new();
|
private AutoBidder.Utilities.AppSettings settings = new();
|
||||||
private System.Threading.Timer? updateTimer;
|
private System.Threading.Timer? updateTimer;
|
||||||
|
|
||||||
@@ -437,6 +685,62 @@ private System.Threading.Timer? updateTimer;
|
|||||||
}, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
|
}, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ApplyStrategiesToAllAuctions()
|
||||||
|
{
|
||||||
|
isApplyingToAll = true;
|
||||||
|
applyToAllMessage = null;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Prima salva le impostazioni
|
||||||
|
SaveSettings();
|
||||||
|
|
||||||
|
// Applica le impostazioni di default a tutte le aste
|
||||||
|
var auctions = AuctionMonitor.GetAuctions().ToList();
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
foreach (var auction in auctions)
|
||||||
|
{
|
||||||
|
// Applica impostazioni predefinite
|
||||||
|
auction.BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs;
|
||||||
|
auction.MinPrice = settings.DefaultMinPrice;
|
||||||
|
auction.MaxPrice = settings.DefaultMaxPrice;
|
||||||
|
auction.MinResets = settings.DefaultMinResets;
|
||||||
|
auction.MaxResets = settings.DefaultMaxResets;
|
||||||
|
|
||||||
|
// Resetta override per usare impostazioni globali
|
||||||
|
auction.AdvancedStrategiesEnabled = null;
|
||||||
|
auction.JitterEnabledOverride = null;
|
||||||
|
auction.SoftRetreatEnabledOverride = null;
|
||||||
|
auction.MaxBidsOverride = null;
|
||||||
|
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salva le aste modificate
|
||||||
|
AutoBidder.Utilities.PersistenceManager.SaveAuctions(auctions);
|
||||||
|
|
||||||
|
applyToAllSuccess = true;
|
||||||
|
applyToAllMessage = $"? Strategie applicate a {count} aste con successo!";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
applyToAllSuccess = false;
|
||||||
|
applyToAllMessage = $"Errore: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isApplyingToAll = false;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
// Rimuovi messaggio dopo 5 secondi
|
||||||
|
await Task.Delay(5000);
|
||||||
|
applyToAllMessage = null;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void SyncStartupSelectionsFromSettings()
|
private void SyncStartupSelectionsFromSettings()
|
||||||
{
|
{
|
||||||
if (settings.RememberAuctionStates)
|
if (settings.RememberAuctionStates)
|
||||||
|
|||||||
+366
-19
@@ -2,8 +2,12 @@
|
|||||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||||
@using AutoBidder.Models
|
@using AutoBidder.Models
|
||||||
@using AutoBidder.Services
|
@using AutoBidder.Services
|
||||||
|
@using Microsoft.JSInterop
|
||||||
@inject StatsService StatsService
|
@inject StatsService StatsService
|
||||||
@inject DatabaseService DatabaseService
|
@inject DatabaseService DatabaseService
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject AuctionMonitor AuctionMonitor
|
||||||
|
@inject ApplicationStateService AppState
|
||||||
|
|
||||||
<PageTitle>Statistiche - AutoBidder</PageTitle>
|
<PageTitle>Statistiche - AutoBidder</PageTitle>
|
||||||
|
|
||||||
@@ -55,17 +59,58 @@
|
|||||||
<div class="col-lg-7">
|
<div class="col-lg-7">
|
||||||
<div class="card shadow-sm h-100">
|
<div class="card shadow-sm h-100">
|
||||||
<div class="card-header bg-primary text-white">
|
<div class="card-header bg-primary text-white">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<i class="bi bi-clock-history me-2"></i>
|
<i class="bi bi-clock-history me-2"></i>
|
||||||
Aste Terminate Recenti
|
Aste Terminate (@(filteredAuctions?.Count ?? 0))
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FILTRI -->
|
||||||
|
<div class="card-body border-bottom py-2">
|
||||||
|
<div class="row g-2 align-items-center">
|
||||||
|
<div class="col-md-5">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||||
|
<input type="text" class="form-control" placeholder="Cerca per nome..."
|
||||||
|
@bind="filterName" @bind:event="oninput" @onkeyup="ApplyFilters" />
|
||||||
|
@if (!string.IsNullOrEmpty(filterName))
|
||||||
|
{
|
||||||
|
<button class="btn btn-outline-secondary" @onclick="ClearNameFilter">
|
||||||
|
<i class="bi bi-x"></i>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<select class="form-select form-select-sm" @bind="filterWon" @bind:after="ApplyFilters">
|
||||||
|
<option value="">Tutte</option>
|
||||||
|
<option value="won">Solo Vinte</option>
|
||||||
|
<option value="lost">Solo Perse</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-muted small d-flex align-items-center">
|
||||||
|
<i class="bi bi-info-circle me-1"></i> Clicca intestazioni per ordinare
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
@if (recentAuctions == null || !recentAuctions.Any())
|
@if (filteredAuctions == null || !filteredAuctions.Any())
|
||||||
{
|
{
|
||||||
<div class="text-center py-5 text-muted">
|
<div class="text-center py-5 text-muted">
|
||||||
<i class="bi bi-inbox" style="font-size: 3rem;"></i>
|
<i class="bi bi-inbox" style="font-size: 3rem;"></i>
|
||||||
<p class="mt-3">Nessuna asta terminata salvata</p>
|
<p class="mt-3">
|
||||||
|
@if (!string.IsNullOrEmpty(filterName) || !string.IsNullOrEmpty(filterWon))
|
||||||
|
{
|
||||||
|
<span>Nessun risultato per i filtri applicati</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Nessuna asta terminata salvata</span>
|
||||||
|
}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -74,30 +119,68 @@
|
|||||||
<table class="table table-hover table-sm mb-0">
|
<table class="table table-hover table-sm mb-0">
|
||||||
<thead class="table-light sticky-top">
|
<thead class="table-light sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nome</th>
|
<th class="sortable-header" @onclick='() => SortBy("name")'>
|
||||||
<th class="text-end">Prezzo</th>
|
Nome @GetSortIndicator("name")
|
||||||
<th class="text-end">Puntate</th>
|
</th>
|
||||||
|
<th class="text-end sortable-header" @onclick='() => SortBy("price")'>
|
||||||
|
Prezzo @GetSortIndicator("price")
|
||||||
|
</th>
|
||||||
|
<th class="text-end sortable-header" @onclick='() => SortBy("bids")'>
|
||||||
|
Puntate @GetSortIndicator("bids")
|
||||||
|
</th>
|
||||||
<th>Vincitore</th>
|
<th>Vincitore</th>
|
||||||
<th class="text-center">Stato</th>
|
<th class="text-center sortable-header" @onclick='() => SortBy("won")'>
|
||||||
<th>Data</th>
|
Stato @GetSortIndicator("won")
|
||||||
|
</th>
|
||||||
|
<th class="text-center sortable-header" @onclick='() => SortBy("resets")'>
|
||||||
|
Heat @GetSortIndicator("resets")
|
||||||
|
</th>
|
||||||
|
<th class="sortable-header" @onclick='() => SortBy("date")'>
|
||||||
|
Data @GetSortIndicator("date")
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var auction in recentAuctions)
|
@foreach (var auction in filteredAuctions)
|
||||||
{
|
{
|
||||||
<tr class="@(auction.Won ? "table-success-subtle" : "")">
|
<tr class="@(auction.Won ? "table-success-subtle" : "") auction-row"
|
||||||
<td><small>@auction.AuctionName</small></td>
|
@onclick="() => SelectAuction(auction)">
|
||||||
|
<td>
|
||||||
|
<small class="fw-bold">@TruncateName(auction.AuctionName, 30)</small>
|
||||||
|
@if (auction.TotalResets > 0)
|
||||||
|
{
|
||||||
|
<br/><small class="text-muted">@auction.TotalResets reset</small>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td class="text-end fw-bold">€@auction.FinalPrice.ToString("F2")</td>
|
<td class="text-end fw-bold">€@auction.FinalPrice.ToString("F2")</td>
|
||||||
<td class="text-end">@auction.BidsUsed</td>
|
<td class="text-end">
|
||||||
|
@auction.BidsUsed
|
||||||
|
@if (auction.WinnerBidsUsed.HasValue && auction.WinnerBidsUsed != auction.BidsUsed)
|
||||||
|
{
|
||||||
|
<small class="text-muted">/@auction.WinnerBidsUsed</small>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td><small class="text-muted">@(auction.WinnerUsername ?? "-")</small></td>
|
<td><small class="text-muted">@(auction.WinnerUsername ?? "-")</small></td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
@if (auction.Won)
|
@if (auction.Won)
|
||||||
{
|
{
|
||||||
<span class="badge bg-success">? Vinta</span>
|
<span class="badge bg-success">?</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="badge bg-secondary">? Persa</span>
|
<span class="badge bg-secondary">?</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
@if (auction.TotalResets.HasValue && auction.TotalResets > 0)
|
||||||
|
{
|
||||||
|
<span class="badge @GetHeatBadgeClass((int)(auction.TotalResets / 10.0 * 100))">
|
||||||
|
@(auction.TotalResets / 10.0 * 100 > 100 ? 100 : auction.TotalResets / 10.0 * 100)%
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="text-muted">-</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td><small class="text-muted">@FormatTimestamp(auction.Timestamp)</small></td>
|
<td><small class="text-muted">@FormatTimestamp(auction.Timestamp)</small></td>
|
||||||
@@ -198,17 +281,159 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- PANNELLO DETTAGLI ASTA SELEZIONATA -->
|
||||||
|
@if (selectedAuctionDetail != null)
|
||||||
|
{
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-info text-white d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
Dettagli Asta: @selectedAuctionDetail.AuctionName
|
||||||
|
</h5>
|
||||||
|
<button class="btn btn-sm btn-outline-light" @onclick="() => selectedAuctionDetail = null">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Colonna 1: Info Base -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h6 class="text-muted mb-3"><i class="bi bi-tag"></i> Informazioni Base</h6>
|
||||||
|
<table class="table table-sm table-borderless">
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">ID Asta:</td>
|
||||||
|
<td class="fw-bold">@selectedAuctionDetail.AuctionId</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Prezzo Finale:</td>
|
||||||
|
<td class="fw-bold text-success">€@selectedAuctionDetail.FinalPrice.ToString("F2")</td>
|
||||||
|
</tr>
|
||||||
|
@if (selectedAuctionDetail.BuyNowPrice.HasValue)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Valore Prodotto:</td>
|
||||||
|
<td>€@selectedAuctionDetail.BuyNowPrice.Value.ToString("F2")</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@if (selectedAuctionDetail.Savings.HasValue)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Risparmio:</td>
|
||||||
|
<td class="text-success fw-bold">€@selectedAuctionDetail.Savings.Value.ToString("F2")</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Risultato:</td>
|
||||||
|
<td>
|
||||||
|
@if (selectedAuctionDetail.Won)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">? VINTA</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-secondary">? Persa</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Vincitore:</td>
|
||||||
|
<td class="fw-bold">@(selectedAuctionDetail.WinnerUsername ?? "-")</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Data Chiusura:</td>
|
||||||
|
<td>@FormatTimestamp(selectedAuctionDetail.Timestamp)</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonna 2: Statistiche Puntate -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h6 class="text-muted mb-3"><i class="bi bi-hand-index-thumb"></i> Statistiche Puntate</h6>
|
||||||
|
<table class="table table-sm table-borderless">
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Le mie puntate:</td>
|
||||||
|
<td class="fw-bold">@selectedAuctionDetail.BidsUsed</td>
|
||||||
|
</tr>
|
||||||
|
@if (selectedAuctionDetail.WinnerBidsUsed.HasValue)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Puntate vincitore:</td>
|
||||||
|
<td>@selectedAuctionDetail.WinnerBidsUsed</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@if (selectedAuctionDetail.TotalResets.HasValue)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Reset totali:</td>
|
||||||
|
<td>@selectedAuctionDetail.TotalResets</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@if (selectedAuctionDetail.ClosedAtHour.HasValue)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Ora chiusura:</td>
|
||||||
|
<td>@selectedAuctionDetail.ClosedAtHour:00</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonna 3: Costi -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h6 class="text-muted mb-3"><i class="bi bi-currency-euro"></i> Analisi Costi</h6>
|
||||||
|
<table class="table table-sm table-borderless">
|
||||||
|
@if (selectedAuctionDetail.ShippingCost.HasValue)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Spedizione:</td>
|
||||||
|
<td>€@selectedAuctionDetail.ShippingCost.Value.ToString("F2")</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@if (selectedAuctionDetail.TotalCost.HasValue)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Costo totale:</td>
|
||||||
|
<td class="fw-bold">€@selectedAuctionDetail.TotalCost.Value.ToString("F2")</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
var bidCost = selectedAuctionDetail.BidsUsed * 0.15;
|
||||||
|
}
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Costo puntate (~):</td>
|
||||||
|
<td>€@bidCost.ToString("F2")</td>
|
||||||
|
</tr>
|
||||||
|
@if (selectedAuctionDetail.ProductKey != null)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">Chiave prodotto:</td>
|
||||||
|
<td><small class="text-muted">@selectedAuctionDetail.ProductKey</small></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private bool isLoading = true;
|
private bool isLoading = true;
|
||||||
private List<AuctionResultExtended>? recentAuctions;
|
private List<AuctionResultExtended>? recentAuctions;
|
||||||
|
private List<AuctionResultExtended>? filteredAuctions;
|
||||||
private List<ProductStatisticsRecord>? products;
|
private List<ProductStatisticsRecord>? products;
|
||||||
|
|
||||||
[Inject] private AuctionMonitor AuctionMonitor { get; set; } = default!;
|
// Filtri e ordinamento
|
||||||
[Inject] private ApplicationStateService AppState { get; set; } = default!;
|
private string filterName = "";
|
||||||
[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
|
private string filterWon = "";
|
||||||
|
private AuctionResultExtended? selectedAuctionDetail;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -222,8 +447,9 @@ private List<ProductStatisticsRecord>? products;
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Carica aste recenti (ultime 50)
|
// Carica aste recenti (ultime 100 per permettere filtri)
|
||||||
recentAuctions = await DatabaseService.GetRecentAuctionResultsAsync(50);
|
recentAuctions = await DatabaseService.GetRecentAuctionResultsAsync(100);
|
||||||
|
ApplyFilters();
|
||||||
|
|
||||||
// Carica prodotti con statistiche
|
// Carica prodotti con statistiche
|
||||||
products = await DatabaseService.GetAllProductStatisticsAsync();
|
products = await DatabaseService.GetAllProductStatisticsAsync();
|
||||||
@@ -239,6 +465,103 @@ private List<ProductStatisticsRecord>? products;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ApplyFilters()
|
||||||
|
{
|
||||||
|
if (recentAuctions == null)
|
||||||
|
{
|
||||||
|
filteredAuctions = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var filtered = recentAuctions.AsEnumerable();
|
||||||
|
|
||||||
|
// Filtro per nome
|
||||||
|
if (!string.IsNullOrWhiteSpace(filterName))
|
||||||
|
{
|
||||||
|
filtered = filtered.Where(a =>
|
||||||
|
a.AuctionName.Contains(filterName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro per stato
|
||||||
|
if (filterWon == "won")
|
||||||
|
{
|
||||||
|
filtered = filtered.Where(a => a.Won);
|
||||||
|
}
|
||||||
|
else if (filterWon == "lost")
|
||||||
|
{
|
||||||
|
filtered = filtered.Where(a => !a.Won);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordinamento
|
||||||
|
filtered = sortColumn switch
|
||||||
|
{
|
||||||
|
"date" => sortDescending ? filtered.OrderByDescending(a => a.Timestamp) : filtered.OrderBy(a => a.Timestamp),
|
||||||
|
"price" => sortDescending ? filtered.OrderByDescending(a => a.FinalPrice) : filtered.OrderBy(a => a.FinalPrice),
|
||||||
|
"bids" => sortDescending ? filtered.OrderByDescending(a => a.BidsUsed) : filtered.OrderBy(a => a.BidsUsed),
|
||||||
|
"name" => sortDescending ? filtered.OrderByDescending(a => a.AuctionName) : filtered.OrderBy(a => a.AuctionName),
|
||||||
|
"won" => sortDescending ? filtered.OrderByDescending(a => a.Won) : filtered.OrderBy(a => a.Won),
|
||||||
|
"resets" => sortDescending ? filtered.OrderByDescending(a => a.TotalResets ?? 0) : filtered.OrderBy(a => a.TotalResets ?? 0),
|
||||||
|
_ => filtered.OrderByDescending(a => a.Timestamp) // date_desc default
|
||||||
|
};
|
||||||
|
|
||||||
|
filteredAuctions = filtered.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearNameFilter()
|
||||||
|
{
|
||||||
|
filterName = "";
|
||||||
|
ApplyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string sortColumn = "date";
|
||||||
|
private bool sortDescending = true;
|
||||||
|
|
||||||
|
private void SortBy(string column)
|
||||||
|
{
|
||||||
|
if (sortColumn == column)
|
||||||
|
{
|
||||||
|
// Toggle direzione se stessa colonna
|
||||||
|
sortDescending = !sortDescending;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Nuova colonna, default discendente
|
||||||
|
sortColumn = column;
|
||||||
|
sortDescending = true;
|
||||||
|
}
|
||||||
|
ApplyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
private MarkupString GetSortIndicator(string column)
|
||||||
|
{
|
||||||
|
if (sortColumn != column)
|
||||||
|
return new MarkupString("<i class=\"bi bi-chevron-expand text-muted\" style=\"font-size: 0.7rem;\"></i>");
|
||||||
|
|
||||||
|
return sortDescending
|
||||||
|
? new MarkupString("<i class=\"bi bi-chevron-down\"></i>")
|
||||||
|
: new MarkupString("<i class=\"bi bi-chevron-up\"></i>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectAuction(AuctionResultExtended auction)
|
||||||
|
{
|
||||||
|
selectedAuctionDetail = auction;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetHeatBadgeClass(int heat)
|
||||||
|
{
|
||||||
|
if (heat < 30) return "bg-success";
|
||||||
|
if (heat < 60) return "bg-warning text-dark";
|
||||||
|
return "bg-danger";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string TruncateName(string name, int maxLength)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(name)) return "-";
|
||||||
|
if (name.Length <= maxLength) return name;
|
||||||
|
return name.Substring(0, maxLength) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
private string FormatTimestamp(string timestamp)
|
private string FormatTimestamp(string timestamp)
|
||||||
{
|
{
|
||||||
if (DateTime.TryParse(timestamp, out var dt))
|
if (DateTime.TryParse(timestamp, out var dt))
|
||||||
@@ -288,3 +611,27 @@ private List<ProductStatisticsRecord>? products;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.sortable-header {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-header:hover {
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auction-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auction-row:hover {
|
||||||
|
background-color: rgba(0,123,255,0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-success-subtle {
|
||||||
|
background-color: rgba(25, 135, 84, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
+3
-1
@@ -176,9 +176,11 @@ var htmlCacheService = new HtmlCacheService(
|
|||||||
maxRetries: 2
|
maxRetries: 2
|
||||||
);
|
);
|
||||||
|
|
||||||
var auctionMonitor = new AuctionMonitor();
|
var bidStrategyService = new BidStrategyService();
|
||||||
|
var auctionMonitor = new AuctionMonitor(bidStrategyService);
|
||||||
htmlCacheService.OnLog += (msg) => Console.WriteLine(msg);
|
htmlCacheService.OnLog += (msg) => Console.WriteLine(msg);
|
||||||
|
|
||||||
|
builder.Services.AddSingleton(bidStrategyService);
|
||||||
builder.Services.AddSingleton(auctionMonitor);
|
builder.Services.AddSingleton(auctionMonitor);
|
||||||
builder.Services.AddSingleton(htmlCacheService);
|
builder.Services.AddSingleton(htmlCacheService);
|
||||||
builder.Services.AddSingleton(sp => new SessionService(auctionMonitor.GetApiClient()));
|
builder.Services.AddSingleton(sp => new SessionService(auctionMonitor.GetApiClient()));
|
||||||
|
|||||||
@@ -1,211 +0,0 @@
|
|||||||
# ?? QUICK START - AutoBidder v1.2.0 con Autenticazione
|
|
||||||
|
|
||||||
## ? Deploy Rapido (5 minuti)
|
|
||||||
|
|
||||||
### Step 1: Configura Password Admin (30 secondi)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Copia template
|
|
||||||
cp .env.example .env
|
|
||||||
|
|
||||||
# Modifica password admin
|
|
||||||
nano .env
|
|
||||||
|
|
||||||
# Imposta:
|
|
||||||
ADMIN_PASSWORD=TuaPasswordSicura123!
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nota:** Le credenziali Bidoo NON servono! Il cookie di sessione si configura dall'interfaccia web.
|
|
||||||
|
|
||||||
### Step 2: Pubblica Immagine (2 minuti)
|
|
||||||
|
|
||||||
**Visual Studio:**
|
|
||||||
- Tasto destro progetto ? **Pubblica**
|
|
||||||
- Seleziona: **GiteaRegistry**
|
|
||||||
- Click **Pubblica**
|
|
||||||
|
|
||||||
**Oppure CLI:**
|
|
||||||
```bash
|
|
||||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Deploy su Unraid (2 minuti)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Docker ? Add Container
|
|
||||||
2. Repository: gitea.encke-hake.ts.net/alby96/autobidder:1.2.0
|
|
||||||
3. Port: 8889 (host) ? 8080 (container)
|
|
||||||
4. Volume: /mnt/user/appdata/autobidder/data ? /app/Data
|
|
||||||
|
|
||||||
5. Environment Variables:
|
|
||||||
ADMIN_USERNAME=admin
|
|
||||||
ADMIN_PASSWORD=TuaPasswordSicura123!
|
|
||||||
|
|
||||||
6. Apply ? Start
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Primo Login (30 secondi)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Browser: http://192.168.30.23:8889
|
|
||||||
2. Redirect automatico a /login
|
|
||||||
3. Username: admin
|
|
||||||
4. Password: TuaPasswordSicura123!
|
|
||||||
5. Click "Accedi"
|
|
||||||
6. ? Homepage AutoBidder!
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 5: Configura Sessione Bidoo (1 minuto)
|
|
||||||
|
|
||||||
**Dopo il primo login:**
|
|
||||||
|
|
||||||
1. Vai su **Settings** ? **Sessione Bidoo**
|
|
||||||
2. Incolla il cookie di sessione ottenuto da Bidoo.it
|
|
||||||
3. Salva
|
|
||||||
|
|
||||||
**Come ottenere il cookie Bidoo:**
|
|
||||||
- Browser ? Bidoo.it ? Login
|
|
||||||
- F12 ? Application ? Cookies
|
|
||||||
- Copia valore cookie di sessione
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Credenziali Richieste
|
|
||||||
|
|
||||||
### 1. Autenticazione Applicazione (SOLO AutoBidder)
|
|
||||||
|
|
||||||
```
|
|
||||||
ADMIN_USERNAME=admin
|
|
||||||
ADMIN_PASSWORD=MyS3cur3P@ss!2024
|
|
||||||
```
|
|
||||||
|
|
||||||
**Requisiti password:**
|
|
||||||
- ? Min 12 caratteri
|
|
||||||
- ? Maiuscole + minuscole
|
|
||||||
- ? Numeri
|
|
||||||
- ? Simboli
|
|
||||||
|
|
||||||
### 2. Sessione Bidoo (Configurata dall'interfaccia web)
|
|
||||||
|
|
||||||
**NON servono credenziali qui!**
|
|
||||||
|
|
||||||
Il cookie di sessione Bidoo si incolla manualmente dall'interfaccia:
|
|
||||||
- Login su AutoBidder
|
|
||||||
- Settings ? Sessione Bidoo
|
|
||||||
- Incolla cookie
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Credenziali Default (Se non configuri ADMIN_PASSWORD)
|
|
||||||
|
|
||||||
**?? SOLO PER TEST LOCALE!**
|
|
||||||
|
|
||||||
**Autenticazione app:**
|
|
||||||
```
|
|
||||||
Username: admin
|
|
||||||
Password: Admin@Password123!
|
|
||||||
```
|
|
||||||
|
|
||||||
**?? Container mostrerà WARNING:**
|
|
||||||
```
|
|
||||||
[Identity] WARNING: ADMIN_PASSWORD not set! Using default password.
|
|
||||||
[Identity] CHANGE IT IMMEDIATELY after first login!
|
|
||||||
[Bidoo] ERROR: BIDOO_USERNAME or BIDOO_PASSWORD not configured!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Verifica Installazione
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Controlla log
|
|
||||||
docker logs AutoBidder | grep "\[Identity\]"
|
|
||||||
|
|
||||||
# Output atteso:
|
|
||||||
[Identity] Database initialized
|
|
||||||
[Identity] Admin user created: admin
|
|
||||||
|
|
||||||
# 2. Test login
|
|
||||||
curl -I http://192.168.30.23:8889
|
|
||||||
|
|
||||||
# Output atteso:
|
|
||||||
HTTP/1.1 302 Found
|
|
||||||
Location: /login
|
|
||||||
|
|
||||||
# 3. Test dopo login
|
|
||||||
# Browser ? Homepage deve essere accessibile ?
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Troubleshooting Rapido
|
|
||||||
|
|
||||||
### Problema: "Account temporaneamente bloccato"
|
|
||||||
|
|
||||||
```
|
|
||||||
Causa: 5 tentativi falliti
|
|
||||||
Soluzione: Aspetta 15 minuti
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problema: Pagina non carica
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verifica porta container
|
|
||||||
docker logs AutoBidder | grep "listening"
|
|
||||||
# Deve mostrare: Now listening on: http://[::]:8080
|
|
||||||
|
|
||||||
# Verifica port mapping
|
|
||||||
docker port AutoBidder
|
|
||||||
# Deve mostrare: 8080/tcp -> 0.0.0.0:8889
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problema: Password non accettata
|
|
||||||
|
|
||||||
```
|
|
||||||
Requisiti:
|
|
||||||
? Min 12 caratteri
|
|
||||||
? Maiuscola
|
|
||||||
? Minuscola
|
|
||||||
? Numero
|
|
||||||
? Simbolo
|
|
||||||
|
|
||||||
Esempio valido: MyS3cur3P@ss!2024
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Deploy Production Checklist
|
|
||||||
|
|
||||||
- [ ] Password forte configurata in `.env`
|
|
||||||
- [ ] `.env` NOT committed to git
|
|
||||||
- [ ] Immagine pubblicata su Gitea (`v1.2.0`)
|
|
||||||
- [ ] Container started con env vars corrette
|
|
||||||
- [ ] Primo login effettuato
|
|
||||||
- [ ] Tailscale ACL configurato (opzionale)
|
|
||||||
- [ ] Backup volume `/app/Data` configurato
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Aiuto
|
|
||||||
|
|
||||||
**Log completi:**
|
|
||||||
```bash
|
|
||||||
docker logs AutoBidder --tail 100
|
|
||||||
```
|
|
||||||
|
|
||||||
**Documentazione:**
|
|
||||||
- [SECURITY.md](SECURITY.md) - Guida completa sicurezza
|
|
||||||
- [CHANGELOG.md](CHANGELOG.md) - Note versione v1.2.0
|
|
||||||
- [README.md](README.md) - Overview progetto
|
|
||||||
|
|
||||||
**Reset completo (se necessario):**
|
|
||||||
```bash
|
|
||||||
docker stop AutoBidder
|
|
||||||
docker rm AutoBidder
|
|
||||||
# Riconfigura password in .env
|
|
||||||
docker run -d ... (comandi step 3)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**?? AutoBidder v1.2.0 - Pronto per produzione con sicurezza Tailscale!**
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
# ? RELEASE v1.1.1 - Fix Porta Container
|
|
||||||
|
|
||||||
## ?? Bug Fix Critico
|
|
||||||
|
|
||||||
**Versione:** `1.1.0` ? **`1.1.1`**
|
|
||||||
**Tipo:** PATCH (bug fix)
|
|
||||||
**Data:** 2025-01-18
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Problema Riscontrato
|
|
||||||
|
|
||||||
### Sintomi
|
|
||||||
- ? Container si avvia senza errori
|
|
||||||
- ? Log mostra "Application started"
|
|
||||||
- ? Pagina web non carica
|
|
||||||
- ? Browser timeout o "connection refused"
|
|
||||||
|
|
||||||
### Diagnosi
|
|
||||||
|
|
||||||
**Log container:**
|
|
||||||
```
|
|
||||||
info: Microsoft.Hosting.Lifetime[14]
|
|
||||||
Now listening on: http://[::]:5000 ? SBAGLIATO!
|
|
||||||
```
|
|
||||||
|
|
||||||
**Configurazione attesa:**
|
|
||||||
```dockerfile
|
|
||||||
ENV ASPNETCORE_URLS=http://+:8080
|
|
||||||
EXPOSE 8080
|
|
||||||
```
|
|
||||||
|
|
||||||
**Port mapping:**
|
|
||||||
```yaml
|
|
||||||
ports:
|
|
||||||
- "5000:8080" # Host ? Container
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problema:** Container ascolta su 5000, ma port mapping cerca 8080 ? **MISMATCH!**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Soluzione Applicata
|
|
||||||
|
|
||||||
### Modifica `Program.cs`
|
|
||||||
|
|
||||||
**Prima (ERRATO):**
|
|
||||||
```csharp
|
|
||||||
builder.WebHost.ConfigureKestrel(options =>
|
|
||||||
{
|
|
||||||
options.ListenAnyIP(8080); // ? Ignorato!
|
|
||||||
// ...
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Dopo (CORRETTO):**
|
|
||||||
```csharp
|
|
||||||
// NO configurazione esplicita HTTP
|
|
||||||
// ASPNETCORE_URLS gestisce tutto
|
|
||||||
|
|
||||||
if (enableHttps)
|
|
||||||
{
|
|
||||||
// Solo configurazione HTTPS opzionale
|
|
||||||
builder.WebHost.ConfigureKestrel(options =>
|
|
||||||
{
|
|
||||||
// Porta 8443 per HTTPS
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Log porta HTTP da ASPNETCORE_URLS
|
|
||||||
Console.WriteLine($"[Kestrel] Listening on: {ASPNETCORE_URLS}");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Risultato
|
|
||||||
|
|
||||||
**Log corretto:**
|
|
||||||
```
|
|
||||||
[Kestrel] HTTPS disabled - running in HTTP-only mode
|
|
||||||
[Kestrel] Listening on: http://+:8080
|
|
||||||
info: Microsoft.Hosting.Lifetime[14]
|
|
||||||
Now listening on: http://[::]:8080 ? CORRETTO!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Come Testare
|
|
||||||
|
|
||||||
### 1. Rebuild Container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Stop container vecchio
|
|
||||||
docker stop autobidder
|
|
||||||
docker rm autobidder
|
|
||||||
|
|
||||||
# Pull versione 1.1.1
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.1.1
|
|
||||||
|
|
||||||
# Oppure build locale
|
|
||||||
docker build -t autobidder:1.1.1 .
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Avvia Container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name autobidder \
|
|
||||||
-p 5000:8080 \
|
|
||||||
-v /data:/app/Data \
|
|
||||||
gitea.encke-hake.ts.net/alby96/autobidder:1.1.1
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Verifica Log
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs autobidder | grep "Now listening"
|
|
||||||
|
|
||||||
# Output atteso:
|
|
||||||
# Now listening on: http://[::]:8080 ?
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Test Accesso
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Apri browser
|
|
||||||
http://localhost:5000
|
|
||||||
|
|
||||||
# Dovrebbe caricare la homepage AutoBidder ?
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Modificati
|
|
||||||
|
|
||||||
| File | Modifica | Motivo |
|
|
||||||
|------|----------|--------|
|
|
||||||
| `Program.cs` | Rimossa configurazione esplicita porta HTTP | Fix conflitto Kestrel |
|
|
||||||
| `AutoBidder.csproj` | Versione `1.1.1` | Incremento PATCH |
|
|
||||||
| `Dockerfile` | Label version `1.1.1` | Metadata immagine |
|
|
||||||
| `CHANGELOG.md` | Entry v1.1.1 | Documentazione fix |
|
|
||||||
| `FIX_PORTA_CONTAINER.md` | Nuovo documento | Troubleshooting dettagliato |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Migrazione da v1.1.0
|
|
||||||
|
|
||||||
**Nessuna breaking change!**
|
|
||||||
|
|
||||||
Aggiornamento semplice:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Docker
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.1.1
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Unraid
|
|
||||||
# Cambia tag immagine: latest ? 1.1.1
|
|
||||||
# Restart container
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Documentazione
|
|
||||||
|
|
||||||
### Nuovi Documenti
|
|
||||||
|
|
||||||
- **`FIX_PORTA_CONTAINER.md`** - Troubleshooting dettagliato problema porta
|
|
||||||
- Diagnosi completa
|
|
||||||
- Soluzione passo-passo
|
|
||||||
- Test e verifica
|
|
||||||
- Override porta avanzato
|
|
||||||
|
|
||||||
### Documenti Aggiornati
|
|
||||||
|
|
||||||
- `CHANGELOG.md` - Entry v1.1.1
|
|
||||||
- `README.md` - Badge versione aggiornato
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Benefici Fix
|
|
||||||
|
|
||||||
### Prima (v1.1.0)
|
|
||||||
- ? Container parte ma pagina non carica
|
|
||||||
- ? Port mismatch difficile da diagnosticare
|
|
||||||
- ? Configurazione confusa
|
|
||||||
- ? Conflitti Kestrel vs ASPNETCORE_URLS
|
|
||||||
|
|
||||||
### Dopo (v1.1.1)
|
|
||||||
- ? Container accessibile immediatamente
|
|
||||||
- ? Porta configurata centralmente (ASPNETCORE_URLS)
|
|
||||||
- ? Log chiaro della porta in ascolto
|
|
||||||
- ? Nessun conflitto configurazione
|
|
||||||
- ? Più facile override porta
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Checklist Completata
|
|
||||||
|
|
||||||
- [x] Problema identificato (porta 5000 vs 8080)
|
|
||||||
- [x] Root cause trovata (conflitto configurazione)
|
|
||||||
- [x] Fix applicato (rimossa config esplicita)
|
|
||||||
- [x] Build testata
|
|
||||||
- [x] Versione incrementata (1.1.1)
|
|
||||||
- [x] CHANGELOG aggiornato
|
|
||||||
- [x] Documentazione creata
|
|
||||||
- [x] Immagine pronta per pubblicazione
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Prossimi Passi
|
|
||||||
|
|
||||||
### Pubblica su Gitea
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Da Visual Studio
|
|
||||||
# Tasto destro ? Pubblica ? GiteaRegistry
|
|
||||||
|
|
||||||
# Oppure CLI
|
|
||||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
|
||||||
```
|
|
||||||
|
|
||||||
### Commit e Tag
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .
|
|
||||||
git commit -m "fix: container listening on wrong port (5000 instead of 8080)
|
|
||||||
|
|
||||||
- Remove explicit HTTP configuration from Kestrel
|
|
||||||
- Let ASPNETCORE_URLS control HTTP port
|
|
||||||
- Kestrel config now only for optional HTTPS
|
|
||||||
- Fixes web page not loading when accessing container
|
|
||||||
|
|
||||||
Resolves #XX"
|
|
||||||
|
|
||||||
git tag v1.1.1
|
|
||||||
git push origin docker --tags
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Metriche Fix
|
|
||||||
|
|
||||||
- **Tempo diagnosi:** ~10 minuti
|
|
||||||
- **Tempo fix:** ~5 minuti
|
|
||||||
- **Righe modificate:** ~30 righe
|
|
||||||
- **File modificati:** 5 file
|
|
||||||
- **Documentazione:** 1 nuovo doc + aggiornamenti
|
|
||||||
- **Impatto:** **CRITICO** (container inaccessibile)
|
|
||||||
- **Difficoltà:** **BASSA** (una volta identificato)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Lezioni Apprese
|
|
||||||
|
|
||||||
1. **Configurazione esplicita vs variabili ambiente**
|
|
||||||
- Configurazione esplicita ha precedenza
|
|
||||||
- Può causare conflitti difficili da debuggare
|
|
||||||
- Meglio centralizzare config in env vars
|
|
||||||
|
|
||||||
2. **Verifica sempre i log**
|
|
||||||
- "Now listening on:" mostra porta EFFETTIVA
|
|
||||||
- Può essere diversa da quella configurata
|
|
||||||
- Non fidarsi solo della configurazione
|
|
||||||
|
|
||||||
3. **Port mapping deve corrispondere**
|
|
||||||
- Verifica porta container vs port mapping
|
|
||||||
- Usa `docker port <container>` per verificare
|
|
||||||
- Test endpoint prima di troubleshooting complesso
|
|
||||||
|
|
||||||
4. **Keep It Simple**
|
|
||||||
- Meno configurazione = meno problemi
|
|
||||||
- ASPNETCORE_URLS è il modo standard
|
|
||||||
- ConfigureKestrel solo per casi speciali
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**? v1.1.1 PRONTO - Fix Critico Applicato!**
|
|
||||||
|
|
||||||
Container ora accessibile correttamente sulla porta 8080! ??
|
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
# ? RIEPILOGO COMPLETO - CONFIGURAZIONE DOCKER + GITEA
|
|
||||||
|
|
||||||
## ?? Problemi Risolti
|
|
||||||
|
|
||||||
### 1. ? Convenzione Nomi Registry Gitea
|
|
||||||
**Problema:** Path errato con 4 livelli invece di 3
|
|
||||||
- ? Prima: `gitea.../alby96/mimante/autobidder`
|
|
||||||
- ? Dopo: `gitea.../alby96/autobidder`
|
|
||||||
|
|
||||||
### 2. ? Errore Visual Studio "ContainerBuild"
|
|
||||||
**Problema:** Profilo usava `WebPublishMethod=Docker` senza SDK
|
|
||||||
- ? Prima: Richiede Microsoft.Docker.Sdk
|
|
||||||
- ? Dopo: `WebPublishMethod=Custom` senza dipendenze
|
|
||||||
|
|
||||||
### 3. ? Container HTTPS Crash
|
|
||||||
**Problema:** Kestrel cerca certificati HTTPS inesistenti
|
|
||||||
- ? Prima: HTTPS abilitato di default, crash all'avvio
|
|
||||||
- ? Dopo: HTTP only (8080), HTTPS opzionale
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Modificati
|
|
||||||
|
|
||||||
| File | Modifica | Motivo |
|
|
||||||
|------|----------|--------|
|
|
||||||
| `AutoBidder.csproj` | `<ContainerRegistry>` corretto | Convenzione Gitea 3 livelli |
|
|
||||||
| `AutoBidder.csproj` | Post-build target aggiunto | Push automatico su Gitea |
|
|
||||||
| `GiteaRegistry.pubxml` | `WebPublishMethod=Custom` | Nessuna dipendenza SDK Docker |
|
|
||||||
| `GiteaRegistry.pubxml` | Target `DockerBuild` | Build Docker integrato |
|
|
||||||
| `Program.cs` | `enableHttps=false` default | HTTPS disabilitato in container |
|
|
||||||
| `Program.cs` | Porta `8080` | Standard container HTTP |
|
|
||||||
| `Dockerfile` | `ENV Kestrel__EnableHttps=false` | Conferma HTTP only |
|
|
||||||
| `Dockerfile` | `EXPOSE 8080` | Porta HTTP standard |
|
|
||||||
| `docker-compose.yml` | `5000:8080` port mapping | Host:Container corretto |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Workflow Finale
|
|
||||||
|
|
||||||
### Da Visual Studio (1 Click)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Tasto destro progetto ? Pubblica ? GiteaRegistry
|
|
||||||
2. Visual Studio:
|
|
||||||
?? Build .NET (Release)
|
|
||||||
?? Target DockerBuild (profilo) ? docker build
|
|
||||||
?? Post-build Gitea (csproj) ? tag + push
|
|
||||||
?? ? SUCCESS!
|
|
||||||
```
|
|
||||||
|
|
||||||
### Output Completo
|
|
||||||
|
|
||||||
```
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
? DOCKER BUILD: Building container image ?
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
|
|
||||||
?? Building: autobidder:latest
|
|
||||||
? Docker build completed successfully!
|
|
||||||
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
? POST-BUILD: Pubblicazione su Gitea Container Registry ?
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
|
|
||||||
?? Solution Version: 1.0.0
|
|
||||||
??? Target Tags:
|
|
||||||
• gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
• gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
|
|
||||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
? ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ?
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Configurazione Container
|
|
||||||
|
|
||||||
### Porte
|
|
||||||
|
|
||||||
| Ambiente | Host | Container | Protocollo |
|
|
||||||
|----------|------|-----------|------------|
|
|
||||||
| **Development** | 5001 | 5001 | HTTPS (dev cert) |
|
|
||||||
| **Docker/Production** | 5000 | 8080 | HTTP |
|
|
||||||
| **HTTPS Production** | 443 | 8443 | HTTPS (con cert) |
|
|
||||||
|
|
||||||
### Variabili Ambiente
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Container standard (HTTP only)
|
|
||||||
ASPNETCORE_URLS=http://+:8080
|
|
||||||
ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
Kestrel__EnableHttps=false
|
|
||||||
|
|
||||||
# Con HTTPS (opzionale, richiede certificato)
|
|
||||||
Kestrel__EnableHttps=true
|
|
||||||
Kestrel__Certificates__Default__Path=/certs/cert.pfx
|
|
||||||
Kestrel__Certificates__Default__Password=password
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Deploy su Gitea
|
|
||||||
|
|
||||||
### Immagini Pubblicate
|
|
||||||
|
|
||||||
```
|
|
||||||
gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
**Link Gitea:**
|
|
||||||
```
|
|
||||||
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder
|
|
||||||
```
|
|
||||||
|
|
||||||
### Versionamento Automatico
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- AutoBidder.csproj -->
|
|
||||||
<Version>1.0.1</Version> ? Incrementa qui
|
|
||||||
```
|
|
||||||
|
|
||||||
Pubblica ? Crea automaticamente:
|
|
||||||
- Tag `latest` (aggiornato)
|
|
||||||
- Tag `1.0.1` (nuovo)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Comandi Rapidi
|
|
||||||
|
|
||||||
### Autenticazione Gitea
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker login gitea.encke-hake.ts.net
|
|
||||||
# Username: Alby96
|
|
||||||
# Password: [TOKEN PAT]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build Locale + Test
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build
|
|
||||||
docker build -t autobidder:test .
|
|
||||||
|
|
||||||
# Test locale
|
|
||||||
docker run -p 5000:8080 \
|
|
||||||
-v $(pwd)/Data:/app/Data \
|
|
||||||
autobidder:test
|
|
||||||
|
|
||||||
# Apri: http://localhost:5000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pull da Gitea
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Latest
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
|
|
||||||
# Versione specifica (production)
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deploy Production
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name autobidder \
|
|
||||||
-p 5000:8080 \
|
|
||||||
-v /data/autobidder:/app/Data \
|
|
||||||
-v /logs/autobidder:/app/logs \
|
|
||||||
-e ASPNETCORE_ENVIRONMENT=Production \
|
|
||||||
--restart unless-stopped \
|
|
||||||
gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
docker-compose logs -f autobidder
|
|
||||||
|
|
||||||
# Stop
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Rebuild
|
|
||||||
docker-compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Reverse Proxy (HTTPS in Production)
|
|
||||||
|
|
||||||
### Nginx
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
server_name autobidder.example.com;
|
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
|
||||||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://autobidder:8080;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Traefik
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
autobidder:
|
|
||||||
image: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.routers.autobidder.rule=Host(`autobidder.example.com`)"
|
|
||||||
- "traefik.http.routers.autobidder.tls=true"
|
|
||||||
- "traefik.http.routers.autobidder.tls.certresolver=letsencrypt"
|
|
||||||
- "traefik.http.services.autobidder.loadbalancer.server.port=8080"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Checklist Finale
|
|
||||||
|
|
||||||
### Configurazione
|
|
||||||
|
|
||||||
- [x] Convenzione Gitea corretta (3 livelli)
|
|
||||||
- [x] Versionamento automatico da `.csproj`
|
|
||||||
- [x] HTTPS disabilitato in container
|
|
||||||
- [x] Porta HTTP 8080 (standard)
|
|
||||||
- [x] Post-build push automatico
|
|
||||||
- [x] Profilo Visual Studio senza errori
|
|
||||||
|
|
||||||
### Pubblicazione
|
|
||||||
|
|
||||||
- [x] Build locale funziona
|
|
||||||
- [x] Docker build funziona
|
|
||||||
- [x] Tag Gitea creati (`latest` + versione)
|
|
||||||
- [x] Push su Gitea riuscito
|
|
||||||
- [x] Immagini visibili su Gitea
|
|
||||||
- [x] Visual Studio SUCCESS
|
|
||||||
|
|
||||||
### Container
|
|
||||||
|
|
||||||
- [x] Container si avvia senza errori
|
|
||||||
- [x] HTTP accessibile su porta 8080
|
|
||||||
- [x] Volumi persistenti configurati
|
|
||||||
- [x] Healthcheck funzionante
|
|
||||||
- [x] Logs visibili
|
|
||||||
|
|
||||||
### Documentazione
|
|
||||||
|
|
||||||
- [x] DOCKER_PUBLISH_GUIDE.md completa
|
|
||||||
- [x] PROBLEMA_RISOLTO.md (Visual Studio)
|
|
||||||
- [x] PROBLEMA_HTTPS_RISOLTO.md (Container)
|
|
||||||
- [x] CONFIGURAZIONE_FINALE.md
|
|
||||||
- [x] NUOVO_WORKFLOW_RIEPILOGO.md
|
|
||||||
- [x] Questo riepilogo
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? STATO: TUTTO FUNZIONANTE!
|
|
||||||
|
|
||||||
**Workflow completo e testato:**
|
|
||||||
1. ? Modifica codice
|
|
||||||
2. ? Incrementa versione in `.csproj`
|
|
||||||
3. ? Pubblica da Visual Studio (1 click)
|
|
||||||
4. ? Immagini su Gitea (latest + versione)
|
|
||||||
5. ? Deploy su Unraid/Docker
|
|
||||||
|
|
||||||
**Nessun errore, tutto automatico, versionamento tracciato!** ??
|
|
||||||
@@ -1,376 +0,0 @@
|
|||||||
# ?? RIEPILOGO FINALE - RELEASE v1.1.0
|
|
||||||
|
|
||||||
## ? Lavoro Completato
|
|
||||||
|
|
||||||
### ?? Versione Rilasciata
|
|
||||||
|
|
||||||
**Versione:** `1.1.0` (da `1.0.0`)
|
|
||||||
**Tipo:** MINOR (nuove feature + bug fix)
|
|
||||||
**Data:** 2025-01-18
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Creati (13 nuovi)
|
|
||||||
|
|
||||||
### Documentazione
|
|
||||||
|
|
||||||
1. **`README.md`** - Homepage progetto con badge e quick start
|
|
||||||
2. **`CHANGELOG.md`** - Storico completo modifiche (format standard)
|
|
||||||
3. **`VERSIONING.md`** - Guida sistema versionamento
|
|
||||||
4. **`VERSIONING_IMPLEMENTATO.md`** - Riepilogo implementazione
|
|
||||||
5. **`DOCKER_PUBLISH_GUIDE.md`** - Guida pubblicazione Gitea
|
|
||||||
6. **`CONFIGURAZIONE_FINALE.md`** - Riepilogo configurazione
|
|
||||||
7. **`NUOVO_WORKFLOW_RIEPILOGO.md`** - Dettagli workflow
|
|
||||||
8. **`VERIFICA_CONFIGURAZIONE_GITEA.md`** - Checklist conformità
|
|
||||||
9. **`PROBLEMA_RISOLTO.md`** - Fix errore Visual Studio
|
|
||||||
10. **`PROBLEMA_HTTPS_RISOLTO.md`** - Fix crash container
|
|
||||||
11. **`RIEPILOGO_COMPLETO_FINALE.md`** - Overview tutti i problemi
|
|
||||||
|
|
||||||
### Profili e Script
|
|
||||||
|
|
||||||
12. **`Properties/PublishProfiles/GiteaRegistry.pubxml`** - Profilo pubblicazione Gitea
|
|
||||||
13. **`bump-version.ps1`** - Script PowerShell per incremento versione automatico
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Modificati (4)
|
|
||||||
|
|
||||||
1. **`AutoBidder.csproj`**
|
|
||||||
- Versione aggiornata a `1.1.0`
|
|
||||||
- Post-build target per push Gitea
|
|
||||||
- Convenzione registry corretta
|
|
||||||
|
|
||||||
2. **`Program.cs`**
|
|
||||||
- HTTPS disabilitato di default
|
|
||||||
- Porta HTTP: `8080`
|
|
||||||
- Gestione certificati migliorata
|
|
||||||
|
|
||||||
3. **`Dockerfile`**
|
|
||||||
- Versione label aggiornata
|
|
||||||
- `ENV Kestrel__EnableHttps=false`
|
|
||||||
- Source URL corretto
|
|
||||||
|
|
||||||
4. **`docker-compose.yml`**
|
|
||||||
- Port mapping aggiornato `5000:8080`
|
|
||||||
- Convenzione registry corretta
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Funzionalità Implementate
|
|
||||||
|
|
||||||
### 1. ?? Pubblicazione Automatica su Gitea
|
|
||||||
|
|
||||||
**Workflow completo Visual Studio:**
|
|
||||||
```
|
|
||||||
Tasto destro ? Pubblica ? GiteaRegistry
|
|
||||||
?
|
|
||||||
Build .NET (Release)
|
|
||||||
?
|
|
||||||
Docker build (autobidder:latest)
|
|
||||||
?
|
|
||||||
Tag Gitea (latest + versione)
|
|
||||||
?
|
|
||||||
Push automatico
|
|
||||||
?
|
|
||||||
? SUCCESS!
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output:**
|
|
||||||
- `gitea.encke-hake.ts.net/alby96/autobidder:latest`
|
|
||||||
- `gitea.encke-hake.ts.net/alby96/autobidder:1.1.0`
|
|
||||||
|
|
||||||
### 2. ?? Sistema Versionamento Automatico
|
|
||||||
|
|
||||||
**Semantic Versioning implementato:**
|
|
||||||
- MAJOR: Breaking changes (`1.x.x` ? `2.0.0`)
|
|
||||||
- MINOR: Nuove feature (`1.0.x` ? `1.1.0`)
|
|
||||||
- PATCH: Bug fix (`1.0.0` ? `1.0.1`)
|
|
||||||
|
|
||||||
**Strumenti:**
|
|
||||||
- `bump-version.ps1` - Script automatico incremento
|
|
||||||
- `CHANGELOG.md` - Storico modifiche
|
|
||||||
- `VERSIONING.md` - Guida completa
|
|
||||||
|
|
||||||
### 3. ?? Fix Container HTTPS
|
|
||||||
|
|
||||||
**Problema:**
|
|
||||||
```
|
|
||||||
System.InvalidOperationException: Unable to configure HTTPS endpoint
|
|
||||||
```
|
|
||||||
|
|
||||||
**Soluzione:**
|
|
||||||
- HTTPS disabilitato di default (`Kestrel__EnableHttps=false`)
|
|
||||||
- Porta HTTP standard: `8080`
|
|
||||||
- SSL gestito da reverse proxy
|
|
||||||
|
|
||||||
### 4. ?? Fix Visual Studio
|
|
||||||
|
|
||||||
**Problema:**
|
|
||||||
```
|
|
||||||
Errore MSB4057: target "ContainerBuild" non presente
|
|
||||||
```
|
|
||||||
|
|
||||||
**Soluzione:**
|
|
||||||
- Profilo `Custom` senza dipendenze Docker SDK
|
|
||||||
- Target `DockerBuild` integrato
|
|
||||||
- Workflow senza errori
|
|
||||||
|
|
||||||
### 5. ? Convenzione Gitea Corretta
|
|
||||||
|
|
||||||
**Prima (ERRATO):**
|
|
||||||
```
|
|
||||||
gitea.../alby96/mimante/autobidder (4 livelli)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Dopo (CORRETTO):**
|
|
||||||
```
|
|
||||||
gitea.../alby96/autobidder (3 livelli)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Modifiche Breaking
|
|
||||||
|
|
||||||
### 1. Porta Container
|
|
||||||
|
|
||||||
**Prima:**
|
|
||||||
```bash
|
|
||||||
docker run -p 5000:5000 ...
|
|
||||||
```
|
|
||||||
|
|
||||||
**Dopo:**
|
|
||||||
```bash
|
|
||||||
docker run -p 5000:8080 ...
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. HTTPS
|
|
||||||
|
|
||||||
**Prima:**
|
|
||||||
- HTTPS abilitato di default
|
|
||||||
- Richiede certificati
|
|
||||||
|
|
||||||
**Dopo:**
|
|
||||||
- HTTP only di default
|
|
||||||
- HTTPS opzionale con certificato
|
|
||||||
|
|
||||||
### 3. Path Gitea
|
|
||||||
|
|
||||||
**Prima:**
|
|
||||||
```
|
|
||||||
gitea.../alby96/mimante/autobidder:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
**Dopo:**
|
|
||||||
```
|
|
||||||
gitea.../alby96/autobidder:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Come Usare
|
|
||||||
|
|
||||||
### Incremento Versione Automatico
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Bug fix
|
|
||||||
.\bump-version.ps1 -Type patch # 1.1.0 ? 1.1.1
|
|
||||||
|
|
||||||
# Nuova feature
|
|
||||||
.\bump-version.ps1 -Type minor # 1.1.0 ? 1.2.0
|
|
||||||
|
|
||||||
# Breaking change
|
|
||||||
.\bump-version.ps1 -Type major # 1.1.0 ? 2.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pubblicazione su Gitea
|
|
||||||
|
|
||||||
**Da Visual Studio:**
|
|
||||||
1. Tasto destro progetto ? **Pubblica**
|
|
||||||
2. Seleziona: **`GiteaRegistry`**
|
|
||||||
3. Click **Pubblica**
|
|
||||||
|
|
||||||
**Da CLI:**
|
|
||||||
```bash
|
|
||||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deploy Production
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Pull versione specifica
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.1.0
|
|
||||||
|
|
||||||
# Avvia container
|
|
||||||
docker run -d \
|
|
||||||
--name autobidder \
|
|
||||||
-p 5000:8080 \
|
|
||||||
-v /data:/app/Data \
|
|
||||||
gitea.encke-hake.ts.net/alby96/autobidder:1.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Documentazione Disponibile
|
|
||||||
|
|
||||||
### Guide Utente
|
|
||||||
|
|
||||||
| Documento | Scopo |
|
|
||||||
|-----------|-------|
|
|
||||||
| `README.md` | Homepage progetto, quick start, overview |
|
|
||||||
| `CHANGELOG.md` | Storico modifiche per versione |
|
|
||||||
| `DOCKER_PUBLISH_GUIDE.md` | Guida pubblicazione Gitea step-by-step |
|
|
||||||
|
|
||||||
### Guide Sviluppatore
|
|
||||||
|
|
||||||
| Documento | Scopo |
|
|
||||||
|-----------|-------|
|
|
||||||
| `VERSIONING.md` | Sistema versionamento, workflow release |
|
|
||||||
| `CONFIGURAZIONE_FINALE.md` | Riepilogo configurazione Docker/Gitea |
|
|
||||||
| `NUOVO_WORKFLOW_RIEPILOGO.md` | Dettagli tecnici workflow pubblicazione |
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
|
|
||||||
| Documento | Scopo |
|
|
||||||
|-----------|-------|
|
|
||||||
| `PROBLEMA_RISOLTO.md` | Fix errore Visual Studio |
|
|
||||||
| `PROBLEMA_HTTPS_RISOLTO.md` | Fix crash container HTTPS |
|
|
||||||
| `VERIFICA_CONFIGURAZIONE_GITEA.md` | Checklist conformità |
|
|
||||||
|
|
||||||
### Riepilogo
|
|
||||||
|
|
||||||
| Documento | Scopo |
|
|
||||||
|-----------|-------|
|
|
||||||
| `RIEPILOGO_COMPLETO_FINALE.md` | Overview completa tutti i problemi |
|
|
||||||
| `VERSIONING_IMPLEMENTATO.md` | Dettagli implementazione versioning |
|
|
||||||
| **`RIEPILOGO_RELEASE_v1.1.0.md`** | **Questo documento** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Checklist Completata
|
|
||||||
|
|
||||||
### Configurazione
|
|
||||||
|
|
||||||
- [x] Convenzione Gitea corretta (3 livelli)
|
|
||||||
- [x] Versionamento automatico da `.csproj`
|
|
||||||
- [x] HTTPS disabilitato in container
|
|
||||||
- [x] Porta HTTP 8080 (standard)
|
|
||||||
- [x] Post-build push automatico
|
|
||||||
- [x] Profilo Visual Studio funzionante
|
|
||||||
|
|
||||||
### Pubblicazione
|
|
||||||
|
|
||||||
- [x] Build locale testata
|
|
||||||
- [x] Docker build testato
|
|
||||||
- [x] Tag Gitea creati (`latest` + `1.1.0`)
|
|
||||||
- [x] Push su Gitea riuscito
|
|
||||||
- [x] Immagini visibili su Gitea
|
|
||||||
- [x] Visual Studio SUCCESS
|
|
||||||
|
|
||||||
### Container
|
|
||||||
|
|
||||||
- [x] Container si avvia senza errori
|
|
||||||
- [x] HTTP accessibile su porta 8080
|
|
||||||
- [x] Volumi persistenti configurati
|
|
||||||
- [x] Healthcheck funzionante
|
|
||||||
- [x] Logs visibili
|
|
||||||
|
|
||||||
### Documentazione
|
|
||||||
|
|
||||||
- [x] README.md completo
|
|
||||||
- [x] CHANGELOG.md con v1.1.0
|
|
||||||
- [x] VERSIONING.md con guida
|
|
||||||
- [x] Guide troubleshooting complete
|
|
||||||
- [x] Script automazione versione
|
|
||||||
- [x] Tutti i documenti aggiornati
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Prossimi Passi
|
|
||||||
|
|
||||||
### Immediati
|
|
||||||
|
|
||||||
1. **Commit modifiche:**
|
|
||||||
```bash
|
|
||||||
git add .
|
|
||||||
git commit -m "chore: release v1.1.0
|
|
||||||
|
|
||||||
- Feature: Gitea publishing workflow
|
|
||||||
- Feature: Automatic versioning system
|
|
||||||
- Fix: Visual Studio ContainerBuild error
|
|
||||||
- Fix: Container HTTPS crash
|
|
||||||
- Docs: Complete documentation suite"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Tag release:**
|
|
||||||
```bash
|
|
||||||
git tag v1.1.0
|
|
||||||
git push origin docker --tags
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Verifica pubblicazione:**
|
|
||||||
```
|
|
||||||
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder
|
|
||||||
```
|
|
||||||
|
|
||||||
### Futuro (v1.2.0)
|
|
||||||
|
|
||||||
- [ ] Notifiche email per aste vinte
|
|
||||||
- [ ] Export statistiche CSV/Excel
|
|
||||||
- [ ] Dashboard mobile-responsive
|
|
||||||
- [ ] API REST pubblica
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Metriche Release
|
|
||||||
|
|
||||||
### File
|
|
||||||
|
|
||||||
- **Nuovi:** 13 file documentazione/script
|
|
||||||
- **Modificati:** 4 file sorgente
|
|
||||||
- **Righe totali:** ~3500+ righe documentazione
|
|
||||||
|
|
||||||
### Problemi Risolti
|
|
||||||
|
|
||||||
- ? Errore Visual Studio "ContainerBuild"
|
|
||||||
- ? Crash container certificati HTTPS
|
|
||||||
- ? Convenzione path Gitea errata
|
|
||||||
- ? Mancanza sistema versionamento
|
|
||||||
- ? Workflow pubblicazione manuale
|
|
||||||
|
|
||||||
### Funzionalità Aggiunte
|
|
||||||
|
|
||||||
- ? Pubblicazione automatica Gitea
|
|
||||||
- ? Versionamento semantico
|
|
||||||
- ? Script automazione versione
|
|
||||||
- ? Documentazione completa
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? STATO FINALE
|
|
||||||
|
|
||||||
```
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
? ?
|
|
||||||
? ? RELEASE v1.1.0 COMPLETATA CON SUCCESSO! ?
|
|
||||||
? ?
|
|
||||||
? • Sistema versionamento implementato ?
|
|
||||||
? • Workflow Gitea automatizzato ?
|
|
||||||
? • Container HTTPS fix applicato ?
|
|
||||||
? • Visual Studio funzionante ?
|
|
||||||
? • Documentazione completa ?
|
|
||||||
? ?
|
|
||||||
? ?? Immagini disponibili su: ?
|
|
||||||
? gitea.encke-hake.ts.net/alby96/autobidder:latest ?
|
|
||||||
? gitea.encke-hake.ts.net/alby96/autobidder:1.1.0 ?
|
|
||||||
? ?
|
|
||||||
?????????????????????????????????????????????????????????????????????
|
|
||||||
```
|
|
||||||
|
|
||||||
**?? Sistema pronto per production deployment!**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Data completamento:** 2025-01-18
|
|
||||||
**Versione:** 1.1.0
|
|
||||||
**Tipo release:** MINOR (feature + bugfix)
|
|
||||||
**Stato:** ? PRODUCTION READY
|
|
||||||
@@ -1,427 +0,0 @@
|
|||||||
# ?? RIEPILOGO IMPLEMENTAZIONE SICUREZZA v1.2.0
|
|
||||||
|
|
||||||
## ? IMPLEMENTAZIONE COMPLETATA
|
|
||||||
|
|
||||||
Sistema di autenticazione enterprise-grade implementato in AutoBidder per deploy sicuro su Tailscale.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Cosa È Stato Fatto
|
|
||||||
|
|
||||||
### 1. ? Sistema Autenticazione ASP.NET Core Identity
|
|
||||||
|
|
||||||
**File creati/modificati:**
|
|
||||||
- `Models/ApplicationUser.cs` - Modello utente esteso
|
|
||||||
- `Data/ApplicationDbContext.cs` - DbContext Identity
|
|
||||||
- `Pages/Login.razor` - Pagina login styled
|
|
||||||
- `Pages/Logout.razor` - Pagina logout
|
|
||||||
- `Program.cs` - Configurazione Identity + middleware
|
|
||||||
- `Shared/NavMenu.razor` - Indicatore utente + logout
|
|
||||||
|
|
||||||
### 2. ? Protezione Route
|
|
||||||
|
|
||||||
**Pagine protette con `[Authorize]`:**
|
|
||||||
- ? `Pages/Index.razor` (Monitor Aste)
|
|
||||||
- ? `Pages/FreeBids.razor` (Puntate Gratuite)
|
|
||||||
- ? `Pages/Statistics.razor` (Statistiche)
|
|
||||||
- ? `Pages/Settings.razor` (Impostazioni)
|
|
||||||
- ? `Pages/Health.razor` (Health Check)
|
|
||||||
|
|
||||||
**Pagine pubbliche:**
|
|
||||||
- ? `/login` - Accesso
|
|
||||||
- ? `/logout` - Disconnessione
|
|
||||||
|
|
||||||
### 3. ? Database Identity
|
|
||||||
|
|
||||||
```
|
|
||||||
Percorso: /app/Data/identity.db
|
|
||||||
Tipo: SQLite
|
|
||||||
Persistente: Sì (volume Docker)
|
|
||||||
Inizializzazione: Automatica al primo avvio
|
|
||||||
Seed admin: Automatico con credenziali da env vars
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. ? Configurazione Sicurezza
|
|
||||||
|
|
||||||
**Cookie policy:**
|
|
||||||
```csharp
|
|
||||||
HttpOnly = true // Anti-XSS
|
|
||||||
SameSite = Lax // Anti-CSRF
|
|
||||||
SecurePolicy = SameAsRequest // Tailscale HTTP OK
|
|
||||||
ExpireTimeSpan = 7 days
|
|
||||||
SlidingExpiration = true
|
|
||||||
```
|
|
||||||
|
|
||||||
**Password policy:**
|
|
||||||
```
|
|
||||||
Min Length: 12 caratteri
|
|
||||||
RequireDigit: true
|
|
||||||
RequireLowercase: true
|
|
||||||
RequireUppercase: true
|
|
||||||
RequireNonAlphanumeric: true
|
|
||||||
RequiredUniqueChars: 4
|
|
||||||
```
|
|
||||||
|
|
||||||
**Lockout policy:**
|
|
||||||
```
|
|
||||||
MaxFailedAccessAttempts: 5
|
|
||||||
DefaultLockoutTimeSpan: 15 minuti
|
|
||||||
AllowedForNewUsers: true
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. ? Environment Variables
|
|
||||||
|
|
||||||
**docker-compose.yml:**
|
|
||||||
```yaml
|
|
||||||
environment:
|
|
||||||
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
|
|
||||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
|
||||||
```
|
|
||||||
|
|
||||||
**.env.example:**
|
|
||||||
```bash
|
|
||||||
ADMIN_USERNAME=admin
|
|
||||||
ADMIN_PASSWORD= # DA CONFIGURARE!
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. ? Documentazione
|
|
||||||
|
|
||||||
**File creati:**
|
|
||||||
- `SECURITY.md` - Guida completa sicurezza (comprehensive)
|
|
||||||
- `CHANGELOG.md` - Aggiornato con v1.2.0
|
|
||||||
- `README.md` - Aggiornato con sezione sicurezza
|
|
||||||
- `.env` - File configurazione template
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Come Funziona
|
|
||||||
|
|
||||||
### Flusso Autenticazione
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Utente accede a http://192.168.30.23:8889
|
|
||||||
2. AutoBidder verifica autenticazione
|
|
||||||
3. Se NON autenticato ? redirect /login
|
|
||||||
4. Utente inserisce username/password
|
|
||||||
5. ASP.NET Core Identity valida:
|
|
||||||
- Password policy
|
|
||||||
- Lockout status
|
|
||||||
- Account attivo
|
|
||||||
6. Se valido:
|
|
||||||
- Crea cookie sicuro
|
|
||||||
- Redirect alla pagina richiesta
|
|
||||||
7. Cookie valido per 7 giorni (sliding)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Protezione Brute-Force
|
|
||||||
|
|
||||||
```
|
|
||||||
Tentativo 1-4: Login fallito
|
|
||||||
Tentativo 5: Account lockout (15 min)
|
|
||||||
Tentativo 6: "Account temporarily blocked"
|
|
||||||
Dopo 15 min: Lockout automaticamente rimosso
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gestione Sessioni
|
|
||||||
|
|
||||||
```
|
|
||||||
Cookie lifetime: 7 giorni
|
|
||||||
Sliding expiration: Sì (rinnovo automatico)
|
|
||||||
Inattività max: ~7 giorni
|
|
||||||
Logout: Distruzione cookie immediata
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Configurazione Deployment
|
|
||||||
|
|
||||||
### Unraid
|
|
||||||
|
|
||||||
```
|
|
||||||
Repository: gitea.encke-hake.ts.net/alby96/autobidder:1.2.0
|
|
||||||
|
|
||||||
Port Mappings:
|
|
||||||
Container Port: 8080
|
|
||||||
Host Port: 8889
|
|
||||||
|
|
||||||
Environment Variables:
|
|
||||||
ADMIN_USERNAME=admin
|
|
||||||
ADMIN_PASSWORD=MyS3cur3P@ss!2024
|
|
||||||
ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
|
|
||||||
Volumes:
|
|
||||||
Container Path: /app/Data
|
|
||||||
Host Path: /mnt/user/appdata/autobidder/data
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
autobidder:
|
|
||||||
image: gitea.encke-hake.ts.net/alby96/autobidder:1.2.0
|
|
||||||
ports:
|
|
||||||
- "8889:8080"
|
|
||||||
environment:
|
|
||||||
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
|
|
||||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
|
||||||
volumes:
|
|
||||||
- ./Data:/app/Data
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tailscale
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Esponi su Tailscale (opzionale, per HTTPS)
|
|
||||||
tailscale serve --bg --https=8443 http://localhost:8080
|
|
||||||
|
|
||||||
# Accedi via Tailscale hostname
|
|
||||||
https://autobidder.tailnet-XXXX.ts.net
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Primo Avvio
|
|
||||||
|
|
||||||
### 1. Configura Password
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# .env
|
|
||||||
ADMIN_USERNAME=admin
|
|
||||||
ADMIN_PASSWORD=MyS3cur3P@ssw0rd!2024
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Build Immagine
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -t autobidder:1.2.0 .
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Avvia Container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name AutoBidder \
|
|
||||||
-p 8889:8080 \
|
|
||||||
-e ADMIN_USERNAME=admin \
|
|
||||||
-e ADMIN_PASSWORD="MyS3cur3P@ss!2024" \
|
|
||||||
-v /data:/app/Data \
|
|
||||||
autobidder:1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Verifica Log
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs AutoBidder | grep "\[Identity\]"
|
|
||||||
|
|
||||||
# Output atteso:
|
|
||||||
[Identity] Database initialized
|
|
||||||
[Identity] Admin user created: admin
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Primo Login
|
|
||||||
|
|
||||||
```
|
|
||||||
Browser: http://192.168.30.23:8889
|
|
||||||
?
|
|
||||||
Redirect automatico a /login
|
|
||||||
?
|
|
||||||
Username: admin
|
|
||||||
Password: MyS3cur3P@ss!2024
|
|
||||||
?
|
|
||||||
Click "Accedi"
|
|
||||||
?
|
|
||||||
? Homepage AutoBidder
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Password Temporanea (Default)
|
|
||||||
|
|
||||||
### ?? SE NON CONFIGURI ADMIN_PASSWORD
|
|
||||||
|
|
||||||
**Username:** `admin`
|
|
||||||
**Password:** `Admin@Password123!`
|
|
||||||
|
|
||||||
**WARNING nei log:**
|
|
||||||
```
|
|
||||||
[Identity] WARNING: ADMIN_PASSWORD not set! Using default password.
|
|
||||||
[Identity] CHANGE IT IMMEDIATELY after first login!
|
|
||||||
[Identity] Admin user created: admin
|
|
||||||
[Identity] ?? REMEMBER TO CHANGE THE DEFAULT PASSWORD!
|
|
||||||
```
|
|
||||||
|
|
||||||
**?? CAMBIARE IMMEDIATAMENTE!**
|
|
||||||
|
|
||||||
(Funzione cambio password sarà aggiunta in v1.2.1)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Test Sicurezza
|
|
||||||
|
|
||||||
### Test 1: Login Riuscito
|
|
||||||
|
|
||||||
```
|
|
||||||
Username: admin
|
|
||||||
Password: (corretta)
|
|
||||||
Result: ? Accesso consentito
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 2: Password Sbagliata
|
|
||||||
|
|
||||||
```
|
|
||||||
Username: admin
|
|
||||||
Password: wrong
|
|
||||||
Result: ? "Username o password non validi"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 3: Brute-Force Protection
|
|
||||||
|
|
||||||
```
|
|
||||||
Tentativi: 5x password sbagliata
|
|
||||||
Result: ? "Account temporaneamente bloccato per troppi tentativi falliti"
|
|
||||||
Wait: 15 minuti
|
|
||||||
Result: ? Lockout rimosso, può ritentare
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 4: Protezione Route
|
|
||||||
|
|
||||||
```
|
|
||||||
Browser: http://192.168.30.23:8889/
|
|
||||||
Stato: Non autenticato
|
|
||||||
Result: ? Redirect a /login
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test 5: Sessione Persistente
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Login con "Ricordami" ?
|
|
||||||
2. Chiudi browser
|
|
||||||
3. Riapri dopo 1 ora
|
|
||||||
4. Vai a homepage
|
|
||||||
Result: ? Ancora autenticato (cookie valido)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Checklist Completa
|
|
||||||
|
|
||||||
### Implementazione
|
|
||||||
- [x] ASP.NET Core Identity configurato
|
|
||||||
- [x] ApplicationUser model creato
|
|
||||||
- [x] ApplicationDbContext creato
|
|
||||||
- [x] Pagina Login styled
|
|
||||||
- [x] Pagina Logout
|
|
||||||
- [x] Protezione route con [Authorize]
|
|
||||||
- [x] Cookie sicuri configurati
|
|
||||||
- [x] Password policy forte
|
|
||||||
- [x] Lockout brute-force
|
|
||||||
- [x] Seed utente admin
|
|
||||||
- [x] Environment variables
|
|
||||||
- [x] NavMenu con logout
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
- [x] docker-compose.yml aggiornato
|
|
||||||
- [x] .env.example creato
|
|
||||||
- [x] .env template creato
|
|
||||||
- [x] Healthcheck compatibile
|
|
||||||
- [x] Volume /app/Data persistente
|
|
||||||
- [x] Build test superato
|
|
||||||
|
|
||||||
### Documentazione
|
|
||||||
- [x] SECURITY.md completa
|
|
||||||
- [x] CHANGELOG.md aggiornato
|
|
||||||
- [x] README.md aggiornato
|
|
||||||
- [x] Versione incrementata (1.2.0)
|
|
||||||
- [x] Questo riepilogo
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Creati/Modificati
|
|
||||||
|
|
||||||
### Nuovi File (11)
|
|
||||||
- `Models/ApplicationUser.cs`
|
|
||||||
- `Data/ApplicationDbContext.cs`
|
|
||||||
- `Pages/Login.razor`
|
|
||||||
- `Pages/Logout.razor`
|
|
||||||
- `SECURITY.md`
|
|
||||||
- `RIEPILOGO_SICUREZZA_v1.2.0.md`
|
|
||||||
- `.env`
|
|
||||||
|
|
||||||
### File Modificati (9)
|
|
||||||
- `Program.cs` (Identity + middleware)
|
|
||||||
- `AutoBidder.csproj` (package Identity)
|
|
||||||
- `Shared/NavMenu.razor` (logout + user info)
|
|
||||||
- `Pages/Index.razor` ([Authorize])
|
|
||||||
- `Pages/FreeBids.razor` ([Authorize])
|
|
||||||
- `Pages/Statistics.razor` ([Authorize])
|
|
||||||
- `Pages/Settings.razor` ([Authorize])
|
|
||||||
- `Pages/Health.razor` ([Authorize])
|
|
||||||
- `docker-compose.yml` (env vars)
|
|
||||||
- `.env.example` (credenziali)
|
|
||||||
- `README.md` (sezione sicurezza)
|
|
||||||
- `CHANGELOG.md` (v1.2.0)
|
|
||||||
- `Dockerfile` (versione 1.2.0)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Prossimi Passi
|
|
||||||
|
|
||||||
### Per l'Utente
|
|
||||||
|
|
||||||
1. **Configura password in `.env`:**
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
nano .env # Imposta ADMIN_PASSWORD
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Pubblica nuova immagine:**
|
|
||||||
```bash
|
|
||||||
# Visual Studio ? Pubblica ? GiteaRegistry
|
|
||||||
# Oppure:
|
|
||||||
docker build -t gitea.../autobidder:1.2.0 .
|
|
||||||
docker push gitea.../autobidder:1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Deploy su Unraid:**
|
|
||||||
- Stop container vecchio
|
|
||||||
- Pull immagine `1.2.0`
|
|
||||||
- Aggiungi env vars: `ADMIN_USERNAME`, `ADMIN_PASSWORD`
|
|
||||||
- Start container
|
|
||||||
- Primo login
|
|
||||||
|
|
||||||
### Per lo Sviluppatore (Futuro)
|
|
||||||
|
|
||||||
**v1.2.1:**
|
|
||||||
- [ ] Pagina cambio password utente
|
|
||||||
- [ ] Gestione profilo utente
|
|
||||||
- [ ] Visualizzazione ultimo accesso
|
|
||||||
|
|
||||||
**v1.3.0:**
|
|
||||||
- [ ] Multi-utente (admin + users)
|
|
||||||
- [ ] Ruoli e permessi
|
|
||||||
- [ ] Log audit accessi
|
|
||||||
- [ ] 2FA opzionale
|
|
||||||
|
|
||||||
**v2.0.0:**
|
|
||||||
- [ ] OAuth2/OIDC (Tailscale)
|
|
||||||
- [ ] SSO integration
|
|
||||||
- [ ] LDAP/AD support
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? IMPLEMENTAZIONE COMPLETA E TESTATA!
|
|
||||||
|
|
||||||
**?? AutoBidder v1.2.0** è ora protetto con autenticazione enterprise-grade, pronto per deploy production su Tailscale!
|
|
||||||
|
|
||||||
**Sicurezza implementata:**
|
|
||||||
- ? Login username/password
|
|
||||||
- ? Protezione brute-force
|
|
||||||
- ? Cookie sicuri
|
|
||||||
- ? Password policy forte
|
|
||||||
- ? Protezione route
|
|
||||||
- ? Database Identity persistente
|
|
||||||
- ? Seed admin automatico
|
|
||||||
- ? Documentazione completa
|
|
||||||
|
|
||||||
**?? Pronto per pubblicazione e deployment!**
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
# ? RIMOSSI PARAMETRI CREDENZIALI BIDOO
|
|
||||||
|
|
||||||
## ?? Modifiche Applicate
|
|
||||||
|
|
||||||
### Motivazione
|
|
||||||
|
|
||||||
Le credenziali Bidoo (username/password) **NON sono necessarie** perché l'applicazione usa il **cookie di sessione** incollato manualmente dall'interfaccia web.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Modificati
|
|
||||||
|
|
||||||
### 1. **Dockerfile**
|
|
||||||
```docker
|
|
||||||
# RIMOSSO:
|
|
||||||
ENV BIDOO_USERNAME=
|
|
||||||
ENV BIDOO_PASSWORD=
|
|
||||||
|
|
||||||
# MANTENUTO:
|
|
||||||
ENV ADMIN_USERNAME=admin
|
|
||||||
ENV ADMIN_PASSWORD=
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **docker-compose.yml**
|
|
||||||
```yaml
|
|
||||||
# RIMOSSO:
|
|
||||||
- BIDOO_USERNAME=${BIDOO_USERNAME}
|
|
||||||
- BIDOO_PASSWORD=${BIDOO_PASSWORD}
|
|
||||||
|
|
||||||
# MANTENUTO:
|
|
||||||
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
|
|
||||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. **.env.example**
|
|
||||||
```bash
|
|
||||||
# RIMOSSO:
|
|
||||||
BIDOO_USERNAME=
|
|
||||||
BIDOO_PASSWORD=
|
|
||||||
|
|
||||||
# AGGIUNTO commento:
|
|
||||||
# === NOTA: SESSIONE BIDOO ===
|
|
||||||
# Il cookie si configura dall'interfaccia web
|
|
||||||
# Settings ? Sessione Bidoo ? Incolla cookie
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. **.env**
|
|
||||||
```bash
|
|
||||||
# RIMOSSO:
|
|
||||||
BIDOO_USERNAME=
|
|
||||||
BIDOO_PASSWORD=
|
|
||||||
|
|
||||||
# AGGIUNTO:
|
|
||||||
# === NOTA: SESSIONE BIDOO ===
|
|
||||||
# Si configura dall'interfaccia web
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. **UNRAID_TEMPLATE.md**
|
|
||||||
|
|
||||||
**XML Template - Rimossi parametri:**
|
|
||||||
```xml
|
|
||||||
<!-- RIMOSSO -->
|
|
||||||
<Config Name="Bidoo Username" ...></Config>
|
|
||||||
<Config Name="Bidoo Password" ...></Config>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Documentazione aggiornata:**
|
|
||||||
```markdown
|
|
||||||
#### ?? Sessione Bidoo
|
|
||||||
|
|
||||||
NON servono credenziali qui!
|
|
||||||
|
|
||||||
Il cookie si configura dall'interfaccia web:
|
|
||||||
1. Login su AutoBidder
|
|
||||||
2. Settings ? Sessione Bidoo
|
|
||||||
3. Incolla cookie
|
|
||||||
4. Salva
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. **QUICKSTART_SECURITY.md**
|
|
||||||
|
|
||||||
**Rimossa sezione:**
|
|
||||||
```markdown
|
|
||||||
### 2. Credenziali Bidoo (Funzionamento)
|
|
||||||
BIDOO_USERNAME=...
|
|
||||||
BIDOO_PASSWORD=...
|
|
||||||
```
|
|
||||||
|
|
||||||
**Aggiunto Step 5:**
|
|
||||||
```markdown
|
|
||||||
### Step 5: Configura Sessione Bidoo (1 minuto)
|
|
||||||
|
|
||||||
1. Settings ? Sessione Bidoo
|
|
||||||
2. Incolla cookie
|
|
||||||
3. Salva
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. **SECURITY.md**
|
|
||||||
|
|
||||||
**Esempi aggiornati:**
|
|
||||||
- Rimossi parametri `BIDOO_USERNAME` e `BIDOO_PASSWORD`
|
|
||||||
- Aggiunta nota: "Si configura dall'interfaccia web"
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Configurazione Finale
|
|
||||||
|
|
||||||
### Environment Variables Richieste
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# SOLO AUTENTICAZIONE APPLICAZIONE
|
|
||||||
ADMIN_USERNAME=admin
|
|
||||||
ADMIN_PASSWORD=TuaPasswordSicura123!
|
|
||||||
|
|
||||||
# Database (opzionale)
|
|
||||||
POSTGRES_USER=autobidder
|
|
||||||
POSTGRES_PASSWORD=autobidder_password
|
|
||||||
USE_POSTGRES=true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configurazione Sessione Bidoo
|
|
||||||
|
|
||||||
**Dall'interfaccia web dopo login:**
|
|
||||||
|
|
||||||
1. **Login su AutoBidder**
|
|
||||||
- Username: `admin`
|
|
||||||
- Password: (valore `ADMIN_PASSWORD`)
|
|
||||||
|
|
||||||
2. **Vai su Settings**
|
|
||||||
- Click menu: **Settings**
|
|
||||||
|
|
||||||
3. **Sezione Sessione Bidoo**
|
|
||||||
- Campo: "Cookie di sessione"
|
|
||||||
- Incolla cookie ottenuto da Bidoo.it
|
|
||||||
- Click: **Salva**
|
|
||||||
|
|
||||||
4. **Verifica connessione**
|
|
||||||
- Homepage ? monitoring aste dovrebbe funzionare
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Come Ottenere Cookie Bidoo
|
|
||||||
|
|
||||||
### Browser Desktop
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Apri Bidoo.it
|
|
||||||
2. Fai login con le tue credenziali
|
|
||||||
3. Premi F12 (Developer Tools)
|
|
||||||
4. Tab "Application" (Chrome) o "Storage" (Firefox)
|
|
||||||
5. Cookies ? https://bidoo.it
|
|
||||||
6. Cerca cookie di sessione (es. "session_id", "auth_token")
|
|
||||||
7. Copia il valore
|
|
||||||
8. Incolla in AutoBidder Settings
|
|
||||||
```
|
|
||||||
|
|
||||||
### Chrome Mobile
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Bidoo.it ? Login
|
|
||||||
2. Chrome menu (?) ? More tools ? Developer tools
|
|
||||||
3. Application ? Cookies
|
|
||||||
4. Copia valore cookie sessione
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Unraid - Esempio Configurazione
|
|
||||||
|
|
||||||
### Environment Variables (SOLO ADMIN)
|
|
||||||
|
|
||||||
```
|
|
||||||
ADMIN_USERNAME = admin
|
|
||||||
ADMIN_PASSWORD = MyS3cur3P@ss!2024
|
|
||||||
ASPNETCORE_ENVIRONMENT = Production
|
|
||||||
```
|
|
||||||
|
|
||||||
**NON servono altri parametri!**
|
|
||||||
|
|
||||||
### Primo Avvio
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Start container
|
|
||||||
2. Browser: http://IP:8889
|
|
||||||
3. Login: admin / password
|
|
||||||
4. Settings ? Sessione Bidoo
|
|
||||||
5. Incolla cookie
|
|
||||||
6. ? Monitoring attivo!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Vantaggi Approccio Cookie
|
|
||||||
|
|
||||||
### ? Pro
|
|
||||||
|
|
||||||
- **Più sicuro:** Nessuna password Bidoo memorizzata nel container
|
|
||||||
- **Più semplice:** Meno parametri da configurare
|
|
||||||
- **Più flessibile:** Cookie può essere aggiornato senza restart container
|
|
||||||
- **Più privacy:** Password Bidoo non visibile nei log Docker
|
|
||||||
|
|
||||||
### ?? Contro
|
|
||||||
|
|
||||||
- **Setup manuale:** Utente deve ottenere cookie da browser
|
|
||||||
- **Scadenza:** Cookie potrebbe scadere (ma può essere aggiornato)
|
|
||||||
|
|
||||||
### ?? Scadenza Cookie
|
|
||||||
|
|
||||||
**Se cookie scade:**
|
|
||||||
1. AutoBidder mostrerà errore connessione Bidoo
|
|
||||||
2. Vai su Settings
|
|
||||||
3. Ottieni nuovo cookie da Bidoo.it
|
|
||||||
4. Incolla e salva
|
|
||||||
5. ? Risolto!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Checklist Aggiornamento
|
|
||||||
|
|
||||||
- [x] Rimossi `BIDOO_USERNAME` e `BIDOO_PASSWORD` da Dockerfile
|
|
||||||
- [x] Rimossi da docker-compose.yml
|
|
||||||
- [x] Rimossi da .env.example
|
|
||||||
- [x] Rimossi da .env
|
|
||||||
- [x] Aggiornato UNRAID_TEMPLATE.md (XML + docs)
|
|
||||||
- [x] Aggiornato QUICKSTART_SECURITY.md
|
|
||||||
- [x] Aggiornato SECURITY.md
|
|
||||||
- [x] Aggiunte note "Configurazione dall'interfaccia web"
|
|
||||||
- [x] Documentato come ottenere cookie Bidoo
|
|
||||||
- [x] Build test superato ?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Prossimi Passi
|
|
||||||
|
|
||||||
### Per l'Utente
|
|
||||||
|
|
||||||
1. **Se hai già deployato v1.2.0 con credenziali Bidoo:**
|
|
||||||
- Non serve fare niente!
|
|
||||||
- Parametri `BIDOO_*` verranno ignorati
|
|
||||||
|
|
||||||
2. **Nuovo deploy:**
|
|
||||||
- Configura solo `ADMIN_PASSWORD`
|
|
||||||
- Dopo login, incolla cookie Bidoo in Settings
|
|
||||||
|
|
||||||
### Per lo Sviluppatore
|
|
||||||
|
|
||||||
**Nessuna modifica codice necessaria!**
|
|
||||||
|
|
||||||
L'app già supporta l'incollatura manuale del cookie dall'interfaccia Settings.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? COMPLETATO
|
|
||||||
|
|
||||||
**Configurazione semplificata:**
|
|
||||||
- ? SOLO 2 parametri obbligatori: `ADMIN_USERNAME`, `ADMIN_PASSWORD`
|
|
||||||
- ? Cookie Bidoo configurato dall'interfaccia web
|
|
||||||
- ? Template Unraid pulito e semplice
|
|
||||||
- ? Documentazione aggiornata
|
|
||||||
|
|
||||||
**?? Deploy più facile e sicuro!**
|
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
# ?? GUIDA SICUREZZA - AutoBidder v1.2.0
|
|
||||||
|
|
||||||
## ?? Sistema di Autenticazione Implementato
|
|
||||||
|
|
||||||
AutoBidder v1.2.0 include un sistema di autenticazione completo basato su **ASP.NET Core Identity**, progettato specificamente per l'esposizione sicura tramite Tailscale.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Feature di Sicurezza
|
|
||||||
|
|
||||||
### 1. ?? Autenticazione Utente
|
|
||||||
|
|
||||||
- **ASP.NET Core Identity** integrato
|
|
||||||
- Login con username e password
|
|
||||||
- Sessioni sicure con cookie HttpOnly
|
|
||||||
- Logout sicuro
|
|
||||||
|
|
||||||
### 2. ??? Protezione Brute-Force
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Configurazione automatica:
|
|
||||||
- Max tentativi falliti: 5
|
|
||||||
- Timeout lockout: 15 minuti
|
|
||||||
- Lockout abilitato per tutti gli utenti
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. ?? Password Policy Forte
|
|
||||||
|
|
||||||
**Requisiti obbligatori:**
|
|
||||||
- ? Minimo 12 caratteri
|
|
||||||
- ? Almeno 1 maiuscola
|
|
||||||
- ? Almeno 1 minuscola
|
|
||||||
- ? Almeno 1 numero
|
|
||||||
- ? Almeno 1 simbolo speciale
|
|
||||||
- ? Minimo 4 caratteri unici
|
|
||||||
|
|
||||||
**Esempi password valide:**
|
|
||||||
```
|
|
||||||
? MyS3cur3P@ssw0rd!2024
|
|
||||||
? Admin@SecurePass123!
|
|
||||||
? Bidoo#Manager2024$
|
|
||||||
? password123 (troppo semplice)
|
|
||||||
? Admin123 (manca simbolo, troppo corta)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. ?? Cookie Sicuri
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
Cookie Configuration:
|
|
||||||
- HttpOnly: true (protezione XSS)
|
|
||||||
- SameSite: Lax (protezione CSRF)
|
|
||||||
- SecurePolicy: SameAsRequest (Tailscale HTTP OK)
|
|
||||||
- Durata: 7 giorni (sliding expiration)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. ?? Protezione Route
|
|
||||||
|
|
||||||
Tutte le pagine protette con `[Authorize]`:
|
|
||||||
- `/` (Monitor Aste)
|
|
||||||
- `/freebids` (Puntate Gratuite)
|
|
||||||
- `/statistics` (Statistiche)
|
|
||||||
- `/settings` (Impostazioni)
|
|
||||||
- `/health` (Health Check)
|
|
||||||
|
|
||||||
**Pagine pubbliche:**
|
|
||||||
- `/login` ?
|
|
||||||
- `/logout` ?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Configurazione
|
|
||||||
|
|
||||||
### 1. File `.env` (OBBLIGATORIO)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Copia .env.example in .env
|
|
||||||
cp .env.example .env
|
|
||||||
|
|
||||||
# Modifica password admin:
|
|
||||||
ADMIN_USERNAME=admin
|
|
||||||
ADMIN_PASSWORD=TuaPasswordSicura123!
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nota:** Le credenziali Bidoo NON servono qui. Il cookie di sessione si configura dall'interfaccia web dopo il login.
|
|
||||||
|
|
||||||
### 2. Docker Compose
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
autobidder:
|
|
||||||
environment:
|
|
||||||
# Autenticazione applicazione
|
|
||||||
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
|
|
||||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Sessione Bidoo:** Configurata dall'interfaccia web (Settings).
|
|
||||||
|
|
||||||
### 3. Unraid / Docker Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name AutoBidder \
|
|
||||||
-p 8889:8080 \
|
|
||||||
-e ADMIN_USERNAME=admin \
|
|
||||||
-e ADMIN_PASSWORD="MyS3cur3P@ss!" \
|
|
||||||
-v /data:/app/Data \
|
|
||||||
gitea.../autobidder:1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
**Dopo il primo login:**
|
|
||||||
- Settings ? Sessione Bidoo ? Incolla cookie
|
|
||||||
-e ADMIN_PASSWORD="MyS3cur3P@ss!" \
|
|
||||||
-v /data:/app/Data \
|
|
||||||
gitea.../autobidder:1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Primo Avvio
|
|
||||||
|
|
||||||
### Step 1: Configura Password
|
|
||||||
|
|
||||||
**Opzione A: Password personalizzata (CONSIGLIATO)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# .env
|
|
||||||
ADMIN_USERNAME=admin
|
|
||||||
ADMIN_PASSWORD=MyS3cur3P@ssw0rd!2024
|
|
||||||
```
|
|
||||||
|
|
||||||
**Opzione B: Password temporanea default**
|
|
||||||
|
|
||||||
Se `ADMIN_PASSWORD` non è settata:
|
|
||||||
- Username: `admin`
|
|
||||||
- Password: `Admin@Password123!`
|
|
||||||
- ?? **CAMBIARE IMMEDIATAMENTE!**
|
|
||||||
|
|
||||||
### Step 2: Avvia Container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Verifica Log
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs AutoBidder | grep "\[Identity\]"
|
|
||||||
|
|
||||||
# Output atteso:
|
|
||||||
[Identity] Database initialized
|
|
||||||
[Identity] Admin user created: admin
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Primo Login
|
|
||||||
|
|
||||||
1. Apri browser: `http://192.168.30.23:8889`
|
|
||||||
2. Verrai reindirizzato a `/login`
|
|
||||||
3. Inserisci credenziali:
|
|
||||||
- Username: `admin` (o valore ADMIN_USERNAME)
|
|
||||||
- Password: (valore ADMIN_PASSWORD)
|
|
||||||
4. Click "Accedi"
|
|
||||||
|
|
||||||
**Se password temporanea usata:**
|
|
||||||
- ?? Cambia password IMMEDIATAMENTE!
|
|
||||||
- (Funzione cambio password sarà aggiunta in v1.2.1)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Gestione Utenti
|
|
||||||
|
|
||||||
### Database Identity
|
|
||||||
|
|
||||||
```
|
|
||||||
Percorso: /app/Data/identity.db
|
|
||||||
Tipo: SQLite
|
|
||||||
Tabelle:
|
|
||||||
- Users (utenti applicazione)
|
|
||||||
- Roles (ruoli - futuro)
|
|
||||||
- UserLogins (log accessi - futuro)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backup Database Utenti
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backup manuale
|
|
||||||
docker cp AutoBidder:/app/Data/identity.db ./backup/identity-$(date +%Y%m%d).db
|
|
||||||
|
|
||||||
# Verifica backup
|
|
||||||
sqlite3 ./backup/identity-*.db "SELECT UserName, CreatedAt FROM Users;"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reset Password Admin
|
|
||||||
|
|
||||||
Se hai dimenticato la password:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Stop container
|
|
||||||
docker stop AutoBidder
|
|
||||||
|
|
||||||
# 2. Elimina database Identity
|
|
||||||
docker exec AutoBidder rm /app/Data/identity.db
|
|
||||||
|
|
||||||
# 3. Riconfigura password in .env
|
|
||||||
echo "ADMIN_PASSWORD=NuovaPassword123!" >> .env
|
|
||||||
|
|
||||||
# 4. Restart container (creerà nuovo database)
|
|
||||||
docker start AutoBidder
|
|
||||||
|
|
||||||
# 5. Verifica log
|
|
||||||
docker logs AutoBidder | grep "\[Identity\]"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ??? Best Practices Sicurezza
|
|
||||||
|
|
||||||
### 1. Password Forte
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Genera password sicura (Linux/Mac):
|
|
||||||
openssl rand -base64 32
|
|
||||||
|
|
||||||
# Oppure usa password manager:
|
|
||||||
- LastPass
|
|
||||||
- 1Password
|
|
||||||
- Bitwarden
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Rotazione Periodica
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ogni 90 giorni:
|
|
||||||
1. Genera nuova password
|
|
||||||
2. Aggiorna .env
|
|
||||||
3. Restart container
|
|
||||||
4. Verifica accesso
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Monitoraggio Accessi
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Controlla tentativi falliti:
|
|
||||||
docker logs AutoBidder | grep "password non validi"
|
|
||||||
|
|
||||||
# Controlla lockout:
|
|
||||||
docker logs AutoBidder | grep "temporarily blocked"
|
|
||||||
|
|
||||||
# Controlla accessi riusciti:
|
|
||||||
docker logs AutoBidder | grep "Login successful"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Limitazione Accesso Rete
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Solo Tailscale (consigliato):
|
|
||||||
tailscale serve --bg --https=8443 http://localhost:8080
|
|
||||||
|
|
||||||
# Firewall (se non usi Tailscale):
|
|
||||||
ufw allow from 100.64.0.0/10 to any port 8080 # Solo Tailscale IP
|
|
||||||
ufw deny 8080 # Blocca tutto il resto
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. HTTPS con Reverse Proxy
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
# Nginx su Tailscale
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
server_name autobidder.tailnet-XXXX.ts.net;
|
|
||||||
|
|
||||||
ssl_certificate /etc/tailscale/cert.pem;
|
|
||||||
ssl_certificate_key /etc/tailscale/key.pem;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:8080;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Troubleshooting
|
|
||||||
|
|
||||||
### Problema: "Account temporaneamente bloccato"
|
|
||||||
|
|
||||||
**Causa:** Troppi tentativi falliti (5)
|
|
||||||
|
|
||||||
**Soluzione:**
|
|
||||||
```bash
|
|
||||||
# Aspetta 15 minuti (lockout automatico)
|
|
||||||
# Oppure reset database Identity (vedi sopra)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problema: "Username o password non validi"
|
|
||||||
|
|
||||||
**Verifica:**
|
|
||||||
1. Controlla `.env` per ADMIN_PASSWORD
|
|
||||||
2. Verifica maiuscole/minuscole
|
|
||||||
3. Controlla log container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs AutoBidder | grep "\[Identity\]"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problema: Redirect loop `/login`
|
|
||||||
|
|
||||||
**Causa:** Cookie non accettati dal browser
|
|
||||||
|
|
||||||
**Soluzione:**
|
|
||||||
1. Abilita cookie nel browser
|
|
||||||
2. Usa browser diverso
|
|
||||||
3. Controlla log console browser (F12)
|
|
||||||
|
|
||||||
### Problema: Password non accettata
|
|
||||||
|
|
||||||
**Verifica requisiti:**
|
|
||||||
- ? Min 12 caratteri?
|
|
||||||
- ? Maiuscola presente?
|
|
||||||
- ? Minuscola presente?
|
|
||||||
- ? Numero presente?
|
|
||||||
- ? Simbolo presente?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Metriche Sicurezza
|
|
||||||
|
|
||||||
### Audit Log
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ultimi accessi:
|
|
||||||
docker logs AutoBidder --since 24h | grep "\[Identity\]"
|
|
||||||
|
|
||||||
# Tentativi falliti oggi:
|
|
||||||
docker logs AutoBidder --since 1d | grep "password non validi"
|
|
||||||
|
|
||||||
# Lockout oggi:
|
|
||||||
docker logs AutoBidder --since 1d | grep "temporarily blocked"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Statistiche Utenti
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Connetti al database:
|
|
||||||
docker exec -it AutoBidder sqlite3 /app/Data/identity.db
|
|
||||||
|
|
||||||
# Query utenti:
|
|
||||||
SELECT UserName, CreatedAt, LastLoginAt, IsActive
|
|
||||||
FROM Users;
|
|
||||||
|
|
||||||
# Exit:
|
|
||||||
.exit
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Roadmap Sicurezza
|
|
||||||
|
|
||||||
### v1.2.1 (Prossima)
|
|
||||||
- [ ] Cambio password utente
|
|
||||||
- [ ] Gestione multi-utente
|
|
||||||
- [ ] Ruoli (Admin/User)
|
|
||||||
- [ ] Log audit accessi
|
|
||||||
|
|
||||||
### v1.3.0 (Futuro)
|
|
||||||
- [ ] 2FA (Two-Factor Authentication)
|
|
||||||
- [ ] OAuth2/OIDC (Tailscale)
|
|
||||||
- [ ] IP whitelisting
|
|
||||||
- [ ] Session timeout configurabile
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Checklist Sicurezza
|
|
||||||
|
|
||||||
Prima del deploy production:
|
|
||||||
|
|
||||||
- [ ] Password forte configurata in `.env`
|
|
||||||
- [ ] `.env` in `.gitignore` (non committare!)
|
|
||||||
- [ ] Backup database Identity configurato
|
|
||||||
- [ ] Monitoraggio log attivo
|
|
||||||
- [ ] Tailscale ACL configurato (solo utenti autorizzati)
|
|
||||||
- [ ] Firewall configurato (solo Tailscale)
|
|
||||||
- [ ] Reverse proxy HTTPS (opzionale)
|
|
||||||
- [ ] Password rotation calendar (ogni 90 giorni)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Supporto
|
|
||||||
|
|
||||||
**Problemi di sicurezza:**
|
|
||||||
- Apri issue su Gitea (segnala vulnerabilità in privato)
|
|
||||||
- Controlla log: `docker logs AutoBidder`
|
|
||||||
- Verifica configurazione: `docker inspect AutoBidder`
|
|
||||||
|
|
||||||
**Documentazione:**
|
|
||||||
- `CHANGELOG.md` - Note release
|
|
||||||
- `README.md` - Overview progetto
|
|
||||||
- `DOCKER_PUBLISH_GUIDE.md` - Deployment
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**?? AutoBidder v1.2.0 - Sicuro per produzione con Tailscale!**
|
|
||||||
|
|
||||||
Sistema di autenticazione enterprise-grade per proteggere i tuoi dati di asta.
|
|
||||||
@@ -4,16 +4,19 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AutoBidder.Models;
|
using AutoBidder.Models;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
namespace AutoBidder.Services
|
namespace AutoBidder.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Servizio centrale per monitoraggio aste
|
/// Servizio centrale per monitoraggio aste
|
||||||
/// Sistema di timing ottimizzato: punta solo se necessario, poco prima della scadenza
|
/// Sistema di timing ottimizzato: punta solo se necessario, poco prima della scadenza
|
||||||
|
/// Integra BidStrategyService per strategie avanzate
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AuctionMonitor
|
public class AuctionMonitor
|
||||||
{
|
{
|
||||||
private readonly BidooApiClient _apiClient;
|
private readonly BidooApiClient _apiClient;
|
||||||
|
private readonly BidStrategyService _bidStrategy;
|
||||||
private readonly List<AuctionInfo> _auctions = new();
|
private readonly List<AuctionInfo> _auctions = new();
|
||||||
private CancellationTokenSource? _monitoringCts;
|
private CancellationTokenSource? _monitoringCts;
|
||||||
private Task? _monitoringTask;
|
private Task? _monitoringTask;
|
||||||
@@ -29,9 +32,10 @@ namespace AutoBidder.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action<AuctionInfo, AuctionState, bool>? OnAuctionCompleted;
|
public event Action<AuctionInfo, AuctionState, bool>? OnAuctionCompleted;
|
||||||
|
|
||||||
public AuctionMonitor()
|
public AuctionMonitor(BidStrategyService? bidStrategy = null)
|
||||||
{
|
{
|
||||||
_apiClient = new BidooApiClient();
|
_apiClient = new BidooApiClient();
|
||||||
|
_bidStrategy = bidStrategy ?? new BidStrategyService();
|
||||||
|
|
||||||
_apiClient.OnAuctionLog += (auctionId, message) =>
|
_apiClient.OnAuctionLog += (auctionId, message) =>
|
||||||
{
|
{
|
||||||
@@ -334,6 +338,7 @@ namespace AutoBidder.Services
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task PollAndProcessAuction(AuctionInfo auction, CancellationToken token)
|
private async Task PollAndProcessAuction(AuctionInfo auction, CancellationToken token)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -347,10 +352,17 @@ namespace AutoBidder.Services
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ?? Aggiorna latenza con storico
|
||||||
|
auction.AddLatencyMeasurement(state.PollingLatencyMs);
|
||||||
|
|
||||||
auction.PollingLatencyMs = state.PollingLatencyMs;
|
// ?? Segna tracking dall'inizio se è la prima volta
|
||||||
|
if (!auction.IsTrackedFromStart && auction.BidHistory.Count == 0)
|
||||||
|
{
|
||||||
|
auction.IsTrackedFromStart = true;
|
||||||
|
auction.TrackingStartedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
// ? AGGIORNATO: Aggiorna storia puntate mantenendo quelle vecchie
|
// Aggiorna storia puntate mantenendo quelle vecchie
|
||||||
if (state.RecentBidsHistory != null && state.RecentBidsHistory.Count > 0)
|
if (state.RecentBidsHistory != null && state.RecentBidsHistory.Count > 0)
|
||||||
{
|
{
|
||||||
MergeBidHistory(auction, state.RecentBidsHistory);
|
MergeBidHistory(auction, state.RecentBidsHistory);
|
||||||
@@ -365,6 +377,32 @@ namespace AutoBidder.Services
|
|||||||
|
|
||||||
bool won = state.Status == AuctionStatus.EndedWon;
|
bool won = state.Status == AuctionStatus.EndedWon;
|
||||||
|
|
||||||
|
// ?? FIX: Aggiungi ultima puntata mancante a RecentBids
|
||||||
|
// L'API spesso non include l'ultima puntata nella storia
|
||||||
|
if (!string.IsNullOrEmpty(state.LastBidder) && state.Price > 0)
|
||||||
|
{
|
||||||
|
var lastBidTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
var statePrice = (decimal)state.Price;
|
||||||
|
|
||||||
|
// Verifica se questa puntata non è già presente
|
||||||
|
var alreadyExists = auction.RecentBids.Any(b =>
|
||||||
|
Math.Abs(b.Price - statePrice) < 0.001m &&
|
||||||
|
b.Username.Equals(state.LastBidder, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (!alreadyExists)
|
||||||
|
{
|
||||||
|
auction.RecentBids.Insert(0, new BidHistoryEntry
|
||||||
|
{
|
||||||
|
Username = state.LastBidder,
|
||||||
|
Price = statePrice,
|
||||||
|
Timestamp = lastBidTimestamp,
|
||||||
|
BidType = "Auto"
|
||||||
|
});
|
||||||
|
|
||||||
|
auction.AddLog($"[FIX] Aggiunta ultima puntata mancante: {state.LastBidder} €{state.Price:F2}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auction.IsActive = false;
|
auction.IsActive = false;
|
||||||
auction.LastState = state; // Salva stato finale per statistiche
|
auction.LastState = state; // Salva stato finale per statistiche
|
||||||
auction.AddLog($"[ASTA TERMINATA] {statusMsg}");
|
auction.AddLog($"[ASTA TERMINATA] {statusMsg}");
|
||||||
@@ -464,26 +502,88 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Strategia di puntata ottimizzata: punta solo quando necessario
|
/// Strategia di puntata ottimizzata con BidStrategyService
|
||||||
|
/// Usa: adaptive latency, jitter, dynamic offset, heat metric, competition detection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task ExecuteBidStrategy(AuctionInfo auction, AuctionState state, CancellationToken token)
|
private async Task ExecuteBidStrategy(AuctionInfo auction, AuctionState state, CancellationToken token)
|
||||||
{
|
{
|
||||||
|
var settings = SettingsManager.Load();
|
||||||
|
|
||||||
// Calcola il tempo rimanente in millisecondi
|
// Calcola il tempo rimanente in millisecondi
|
||||||
double timerMs = state.Timer * 1000;
|
double timerMs = state.Timer * 1000;
|
||||||
|
|
||||||
// Se siamo nella finestra di puntata (timer <= BidBeforeDeadlineMs)
|
// ??? CONTROLLO: Se sono già il vincitore, non fare nulla
|
||||||
if (timerMs <= auction.BidBeforeDeadlineMs)
|
|
||||||
{
|
|
||||||
auction.IsAttackInProgress = true;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
auction.AddLog($"[STRATEGIA] Finestra di puntata raggiunta: {timerMs:F0}ms <= {auction.BidBeforeDeadlineMs}ms");
|
|
||||||
|
|
||||||
// ? NUOVO: Controlla se sono già io il vincitore corrente
|
|
||||||
if (state.IsMyBid)
|
if (state.IsMyBid)
|
||||||
{
|
{
|
||||||
auction.AddLog($"[STRATEGIA] SKIP: Sono già il vincitore corrente (ultimo bidder: {state.LastBidder})");
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ?? AGGIORNA METRICHE (solo se strategie avanzate abilitate)
|
||||||
|
if (auction.AdvancedStrategiesEnabled != false)
|
||||||
|
{
|
||||||
|
var session = _apiClient.GetSession();
|
||||||
|
var currentUsername = session?.Username ?? "";
|
||||||
|
|
||||||
|
_bidStrategy.UpdateHeatMetric(auction, settings, currentUsername);
|
||||||
|
|
||||||
|
// Verifica strategie avanzate (soft retreat, competition, probabilistic, etc.)
|
||||||
|
var decision = _bidStrategy.ShouldPlaceBid(auction, state, settings, currentUsername);
|
||||||
|
|
||||||
|
if (!decision.ShouldBid)
|
||||||
|
{
|
||||||
|
auction.AddLog($"[STRATEGY] {decision.Reason}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ?? CALCOLA TIMING OTTIMALE
|
||||||
|
var timing = _bidStrategy.CalculateOptimalTiming(auction, settings);
|
||||||
|
int effectiveOffset = timing.FinalOffsetMs;
|
||||||
|
|
||||||
|
// ?? TIMER-BASED SCHEDULING
|
||||||
|
if (timerMs > effectiveOffset)
|
||||||
|
{
|
||||||
|
// Timer ancora alto ? Schedula puntata futura
|
||||||
|
double delayMs = timerMs - effectiveOffset;
|
||||||
|
|
||||||
|
// Non schedulare se già c'è un task attivo per questa asta
|
||||||
|
if (auction.IsAttackInProgress)
|
||||||
|
{
|
||||||
|
return; // Task già schedulato
|
||||||
|
}
|
||||||
|
|
||||||
|
auction.IsAttackInProgress = true;
|
||||||
|
auction.LastUsedOffsetMs = effectiveOffset;
|
||||||
|
|
||||||
|
// Log con dettagli timing (solo se logging avanzato)
|
||||||
|
if (settings.AdvancedLoggingEnabled)
|
||||||
|
{
|
||||||
|
auction.AddLog($"[TIMING] Timer={timerMs:F0}ms, Offset={effectiveOffset}ms (base={timing.BaseOffsetMs}+lat={timing.LatencyCompensationMs}+dyn={timing.DynamicAdjustmentMs}+jit={timing.JitterMs}) ? Delay={delayMs:F0}ms");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auction.AddLog($"[STRATEGIA] Timer={timerMs:F0}ms ? Puntata tra {delayMs:F0}ms (offset={effectiveOffset}ms)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avvia task asincrono che attende e poi punta
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Attendi il momento esatto
|
||||||
|
await Task.Delay((int)delayMs, token);
|
||||||
|
|
||||||
|
// Verifica che l'asta sia ancora attiva e non in pausa
|
||||||
|
if (!auction.IsActive || auction.IsPaused || token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
auction.AddLog($"[STRATEGIA] Task annullato (asta inattiva/pausa)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica soft retreat
|
||||||
|
if (auction.IsInSoftRetreat)
|
||||||
|
{
|
||||||
|
auction.AddLog($"[STRATEGIA] Task annullato (soft retreat attivo)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,7 +594,55 @@ namespace AutoBidder.Services
|
|||||||
var timeSinceLastBid = DateTime.UtcNow - lastBidTime.Value;
|
var timeSinceLastBid = DateTime.UtcNow - lastBidTime.Value;
|
||||||
if (timeSinceLastBid.TotalMilliseconds < 500)
|
if (timeSinceLastBid.TotalMilliseconds < 500)
|
||||||
{
|
{
|
||||||
auction.AddLog($"[STRATEGIA] Puntata recente di {state.LastBidder} ({timeSinceLastBid.TotalMilliseconds:F0}ms fa), attendo...");
|
auction.AddLog($"[COLLISION] Puntata recente di {state.LastBidder} ({timeSinceLastBid.TotalMilliseconds:F0}ms fa)");
|
||||||
|
_bidStrategy.RecordBidAttempt(auction, false, collision: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auction.AddLog($"[STRATEGIA] Task eseguito ? PUNTA ORA!");
|
||||||
|
|
||||||
|
// Esegui la puntata
|
||||||
|
await ExecuteBid(auction, state, token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
auction.AddLog($"[STRATEGIA] Task cancellato");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
auction.AddLog($"[STRATEGIA ERROR] {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
auction.IsAttackInProgress = false;
|
||||||
|
}
|
||||||
|
}, token);
|
||||||
|
}
|
||||||
|
else if (timerMs > 0 && timerMs <= effectiveOffset)
|
||||||
|
{
|
||||||
|
// Timer già nella finestra ? Punta SUBITO senza delay
|
||||||
|
if (auction.IsAttackInProgress)
|
||||||
|
{
|
||||||
|
return; // Già in corso
|
||||||
|
}
|
||||||
|
|
||||||
|
auction.IsAttackInProgress = true;
|
||||||
|
auction.LastUsedOffsetMs = effectiveOffset;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
auction.AddLog($"[STRATEGIA] Timer già in finestra ({timerMs:F0}ms <= {effectiveOffset}ms) ? PUNTA SUBITO!");
|
||||||
|
|
||||||
|
// Controlla se qualcun altro ha puntato di recente
|
||||||
|
var lastBidTime = GetLastBidTime(auction, state.LastBidder);
|
||||||
|
if (lastBidTime.HasValue)
|
||||||
|
{
|
||||||
|
var timeSinceLastBid = DateTime.UtcNow - lastBidTime.Value;
|
||||||
|
if (timeSinceLastBid.TotalMilliseconds < 500)
|
||||||
|
{
|
||||||
|
auction.AddLog($"[COLLISION] Puntata recente di {state.LastBidder} ({timeSinceLastBid.TotalMilliseconds:F0}ms fa)");
|
||||||
|
_bidStrategy.RecordBidAttempt(auction, false, collision: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -507,40 +655,29 @@ namespace AutoBidder.Services
|
|||||||
auction.IsAttackInProgress = false;
|
auction.IsAttackInProgress = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Se timer <= 0, asta già scaduta ? Non fare nulla
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Esegue la puntata con verifica opzionale dello stato dell'asta
|
/// Esegue la puntata e registra metriche
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task ExecuteBid(AuctionInfo auction, AuctionState state, CancellationToken token)
|
private async Task ExecuteBid(AuctionInfo auction, AuctionState state, CancellationToken token)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Se richiesto, verifica prima che l'asta sia ancora aperta
|
// Esegui la puntata immediatamente
|
||||||
if (auction.CheckAuctionOpenBeforeBid)
|
|
||||||
{
|
|
||||||
auction.AddLog("[PRE-CHECK] Verifica stato asta...");
|
|
||||||
var preCheckState = await _apiClient.PollAuctionStateAsync(auction.AuctionId, auction.OriginalUrl, token);
|
|
||||||
|
|
||||||
if (preCheckState == null)
|
|
||||||
{
|
|
||||||
auction.AddLog("[PRE-CHECK] FALLITO: Nessuna risposta");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preCheckState.Status != AuctionStatus.Running)
|
|
||||||
{
|
|
||||||
auction.AddLog($"[PRE-CHECK] ABORTITO: Asta non running (status: {preCheckState.Status})");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auction.AddLog($"[PRE-CHECK] OK - Timer: {preCheckState.Timer:F3}s");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Esegui la puntata
|
|
||||||
var result = await _apiClient.PlaceBidAsync(auction.AuctionId, auction.OriginalUrl);
|
var result = await _apiClient.PlaceBidAsync(auction.AuctionId, auction.OriginalUrl);
|
||||||
auction.LastClickAt = DateTime.UtcNow;
|
auction.LastClickAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Registra metriche
|
||||||
|
bool isCollision = result.Error?.Contains("timer") == true || result.Error?.Contains("scaduto") == true;
|
||||||
|
_bidStrategy.RecordBidAttempt(auction, result.Success, collision: isCollision);
|
||||||
|
|
||||||
|
if (!result.Success && isCollision)
|
||||||
|
{
|
||||||
|
_bidStrategy.RecordTimerExpired(auction);
|
||||||
|
}
|
||||||
|
|
||||||
// Aggiorna dati puntate da risposta server
|
// Aggiorna dati puntate da risposta server
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
@@ -588,8 +725,25 @@ namespace AutoBidder.Services
|
|||||||
|
|
||||||
private bool ShouldBid(AuctionInfo auction, AuctionState state)
|
private bool ShouldBid(AuctionInfo auction, AuctionState state)
|
||||||
{
|
{
|
||||||
|
var settings = Utilities.SettingsManager.Load();
|
||||||
|
|
||||||
|
// ?? CONTROLLO ANTI-AUTOBID BIDOO (PRIORITÀ MASSIMA)
|
||||||
|
// Bidoo ha un sistema di auto-puntata che si attiva a ~2 secondi.
|
||||||
|
// Aspettiamo che il timer scenda sotto la soglia per lasciare che
|
||||||
|
// gli altri utenti con auto-puntata attiva puntino prima di noi.
|
||||||
|
// Questo ci fa risparmiare puntate perché non puntiamo "troppo presto".
|
||||||
|
if (settings.WaitForAutoBidEnabled && state.Timer > settings.WaitForAutoBidThresholdSeconds)
|
||||||
|
{
|
||||||
|
// Timer ancora sopra la soglia - aspetta che le auto-puntate si attivino
|
||||||
|
if (settings.LogAutoBidWaitSkips)
|
||||||
|
{
|
||||||
|
auction.AddLog($"[AUTOBID] Timer {state.Timer:F2}s > soglia {settings.WaitForAutoBidThresholdSeconds}s - Aspetto auto-puntate Bidoo");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ?? CONTROLLO 0: Verifica convenienza (se dati disponibili)
|
// ?? CONTROLLO 0: Verifica convenienza (se dati disponibili)
|
||||||
// ?? IMPORTANTE: Applica solo se BuyNowPrice è valido (> 0)
|
// IMPORTANTE: Applica solo se BuyNowPrice è valido (> 0)
|
||||||
// Se BuyNowPrice == 0, significa errore scraping - non bloccare le puntate
|
// Se BuyNowPrice == 0, significa errore scraping - non bloccare le puntate
|
||||||
if (auction.BuyNowPrice.HasValue &&
|
if (auction.BuyNowPrice.HasValue &&
|
||||||
auction.BuyNowPrice.Value > 0 &&
|
auction.BuyNowPrice.Value > 0 &&
|
||||||
@@ -607,8 +761,40 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ??? CONTROLLO 1: Limite minimo puntate residue
|
// ?? CONTROLLO ANTI-COLLISIONE: Rileva aste troppo "affollate"
|
||||||
var settings = Utilities.SettingsManager.Load();
|
// Se negli ultimi 10 secondi ci sono state 3+ puntate di utenti diversi, evita
|
||||||
|
var recentBidsThreshold = 10; // secondi
|
||||||
|
var maxActiveBidders = 3; // se 3+ bidder attivi, potrebbe essere troppo affollata
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
var recentBids = auction.RecentBids
|
||||||
|
.Where(b => now - b.Timestamp <= recentBidsThreshold)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var activeBidders = recentBids
|
||||||
|
.Select(b => b.Username)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Count();
|
||||||
|
|
||||||
|
if (activeBidders >= maxActiveBidders)
|
||||||
|
{
|
||||||
|
// Controlla se l'ultimo bidder sono io - se sì, posso continuare
|
||||||
|
var session = _apiClient.GetSession();
|
||||||
|
var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
|
||||||
|
|
||||||
|
if (lastBid != null &&
|
||||||
|
!lastBid.Username.Equals(session?.Username, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
auction.AddLog($"[COMPETITION] Asta affollata: {activeBidders} bidder attivi negli ultimi {recentBidsThreshold}s - SKIP");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* Ignora errori nel controllo competizione */ }
|
||||||
|
|
||||||
|
// ?? CONTROLLO 1: Limite minimo puntate residue
|
||||||
if (settings.MinimumRemainingBids > 0)
|
if (settings.MinimumRemainingBids > 0)
|
||||||
{
|
{
|
||||||
var session = _apiClient.GetSession();
|
var session = _apiClient.GetSession();
|
||||||
@@ -749,16 +935,20 @@ namespace AutoBidder.Services
|
|||||||
{
|
{
|
||||||
// Carica impostazioni per limite massimo
|
// Carica impostazioni per limite massimo
|
||||||
var settings = Utilities.SettingsManager.Load();
|
var settings = Utilities.SettingsManager.Load();
|
||||||
var maxEntries = settings?.MaxBidHistoryEntries ?? 20;
|
var maxEntries = settings?.MaxBidHistoryEntries ?? 50; // Default aumentato a 50
|
||||||
|
|
||||||
|
// ?? FIX: Usa lock per thread-safety
|
||||||
|
lock (auction.RecentBids)
|
||||||
|
{
|
||||||
// Se la lista esistente è vuota, semplicemente copia le nuove
|
// Se la lista esistente è vuota, semplicemente copia le nuove
|
||||||
if (auction.RecentBids.Count == 0)
|
if (auction.RecentBids.Count == 0)
|
||||||
{
|
{
|
||||||
auction.RecentBids = newBids.ToList();
|
auction.RecentBids = newBids.ToList();
|
||||||
|
|
||||||
// Ordina per timestamp DECRESCENTE (più recenti in cima)
|
// ?? FIX: Ordina per timestamp DESC, poi per prezzo DESC (per puntate stesso secondo)
|
||||||
auction.RecentBids = auction.RecentBids
|
auction.RecentBids = auction.RecentBids
|
||||||
.OrderByDescending(b => b.Timestamp)
|
.OrderByDescending(b => b.Timestamp)
|
||||||
|
.ThenByDescending(b => b.Price)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Limita se necessario
|
// Limita se necessario
|
||||||
@@ -789,9 +979,10 @@ namespace AutoBidder.Services
|
|||||||
{
|
{
|
||||||
auction.RecentBids.AddRange(bidsToAdd);
|
auction.RecentBids.AddRange(bidsToAdd);
|
||||||
|
|
||||||
// Ordina per timestamp DECRESCENTE (più recenti in cima)
|
// ?? FIX: Ordina per timestamp DESC, poi per prezzo DESC (per puntate stesso secondo)
|
||||||
auction.RecentBids = auction.RecentBids
|
auction.RecentBids = auction.RecentBids
|
||||||
.OrderByDescending(b => b.Timestamp)
|
.OrderByDescending(b => b.Timestamp)
|
||||||
|
.ThenByDescending(b => b.Price)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Limita al numero massimo di puntate (mantieni le più recenti = prime della lista)
|
// Limita al numero massimo di puntate (mantieni le più recenti = prime della lista)
|
||||||
@@ -806,6 +997,7 @@ namespace AutoBidder.Services
|
|||||||
UpdateBidderStatsFromRecentBids(auction);
|
UpdateBidderStatsFromRecentBids(auction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
auction.AddLog($"[WARN] Errore merge storia puntate: {ex.Message}");
|
auction.AddLog($"[WARN] Errore merge storia puntate: {ex.Message}");
|
||||||
|
|||||||
@@ -0,0 +1,501 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using AutoBidder.Models;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
|
namespace AutoBidder.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Servizio per strategie avanzate di puntata.
|
||||||
|
/// Implementa: adaptive latency, jitter, dynamic offset, heat metric,
|
||||||
|
/// competition detection, soft retreat, probabilistic bidding, opponent profiling.
|
||||||
|
/// </summary>
|
||||||
|
public class BidStrategyService
|
||||||
|
{
|
||||||
|
private readonly Random _random = new();
|
||||||
|
private int _sessionTotalBids = 0;
|
||||||
|
private DateTime _sessionStartedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcola l'offset ottimale per una puntata considerando tutti i fattori
|
||||||
|
/// </summary>
|
||||||
|
public BidTimingResult CalculateOptimalTiming(AuctionInfo auction, AppSettings settings)
|
||||||
|
{
|
||||||
|
var result = new BidTimingResult
|
||||||
|
{
|
||||||
|
BaseOffsetMs = auction.BidBeforeDeadlineMs,
|
||||||
|
FinalOffsetMs = auction.BidBeforeDeadlineMs,
|
||||||
|
ShouldBid = true
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Adaptive Latency Compensation
|
||||||
|
if (settings.AdaptiveLatencyEnabled)
|
||||||
|
{
|
||||||
|
result.LatencyCompensationMs = (int)auction.AverageLatencyMs;
|
||||||
|
result.FinalOffsetMs += result.LatencyCompensationMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Dynamic Offset (basato su heat, storico, volatilità)
|
||||||
|
if (settings.DynamicOffsetEnabled)
|
||||||
|
{
|
||||||
|
var dynamicAdjustment = CalculateDynamicOffset(auction, settings);
|
||||||
|
result.DynamicAdjustmentMs = dynamicAdjustment;
|
||||||
|
result.FinalOffsetMs += dynamicAdjustment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Jitter (randomizzazione)
|
||||||
|
if (settings.JitterEnabled || (auction.JitterEnabledOverride ?? settings.JitterEnabled))
|
||||||
|
{
|
||||||
|
result.JitterMs = _random.Next(-settings.JitterRangeMs, settings.JitterRangeMs + 1);
|
||||||
|
result.FinalOffsetMs += result.JitterMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Clamp ai limiti
|
||||||
|
result.FinalOffsetMs = Math.Clamp(result.FinalOffsetMs, settings.MinimumOffsetMs, settings.MaximumOffsetMs);
|
||||||
|
|
||||||
|
// Salva offset calcolato nell'asta
|
||||||
|
auction.DynamicOffsetMs = result.FinalOffsetMs;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcola offset dinamico basato su heat, storico e volatilità
|
||||||
|
/// </summary>
|
||||||
|
private int CalculateDynamicOffset(AuctionInfo auction, AppSettings settings)
|
||||||
|
{
|
||||||
|
int adjustment = 0;
|
||||||
|
|
||||||
|
// Più l'asta è "calda", più anticipo serve
|
||||||
|
if (auction.HeatMetric > 50)
|
||||||
|
{
|
||||||
|
adjustment += (auction.HeatMetric - 50) / 2; // +0-25ms per heat 50-100
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se ci sono state collisioni recenti, anticipa di più
|
||||||
|
if (auction.ConsecutiveCollisions > 0)
|
||||||
|
{
|
||||||
|
adjustment += auction.ConsecutiveCollisions * 10; // +10ms per ogni collisione
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se la latenza è volatile (alta deviazione), aggiungi margine
|
||||||
|
if (auction.LatencyHistory.Count >= 5)
|
||||||
|
{
|
||||||
|
var avg = auction.LatencyHistory.Average();
|
||||||
|
var variance = auction.LatencyHistory.Sum(x => Math.Pow(x - avg, 2)) / auction.LatencyHistory.Count;
|
||||||
|
var stdDev = Math.Sqrt(variance);
|
||||||
|
|
||||||
|
if (stdDev > 20) // Alta variabilità
|
||||||
|
{
|
||||||
|
adjustment += (int)(stdDev / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return adjustment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggiorna heat metric per un'asta
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateHeatMetric(AuctionInfo auction, AppSettings settings, string currentUsername = "")
|
||||||
|
{
|
||||||
|
if (!settings.CompetitionDetectionEnabled) return;
|
||||||
|
|
||||||
|
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
var windowStart = now - settings.CompetitionWindowSeconds;
|
||||||
|
|
||||||
|
// Conta bidder unici nella finestra temporale (escludo me stesso)
|
||||||
|
var recentBids = auction.RecentBids
|
||||||
|
.Where(b => b.Timestamp >= windowStart)
|
||||||
|
.Where(b => !b.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
auction.ActiveBiddersCount = recentBids
|
||||||
|
.Select(b => b.Username)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Count();
|
||||||
|
|
||||||
|
// Conta collisioni (puntate nello stesso secondo)
|
||||||
|
var bidsBySecond = recentBids
|
||||||
|
.GroupBy(b => b.Timestamp)
|
||||||
|
.Where(g => g.Count() > 1)
|
||||||
|
.Count();
|
||||||
|
|
||||||
|
auction.CollisionCount = bidsBySecond;
|
||||||
|
|
||||||
|
// Calcola heat metric (0-100)
|
||||||
|
// Fattori: bidder attivi (40%), frequenza puntate (30%), collisioni (30%)
|
||||||
|
|
||||||
|
int bidderScore = Math.Min(auction.ActiveBiddersCount * 15, 40); // Max 40 punti
|
||||||
|
int frequencyScore = Math.Min(recentBids.Count * 3, 30); // Max 30 punti
|
||||||
|
int collisionScore = Math.Min(auction.CollisionCount * 10, 30); // Max 30 punti
|
||||||
|
|
||||||
|
auction.HeatMetric = bidderScore + frequencyScore + collisionScore;
|
||||||
|
|
||||||
|
// Identifica bidder aggressivi e situazioni di duello
|
||||||
|
if (settings.OpponentProfilingEnabled)
|
||||||
|
{
|
||||||
|
UpdateAggressiveBidders(auction, settings, currentUsername);
|
||||||
|
DetectDuelSituation(auction, settings, currentUsername);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identifica e tracca bidder aggressivi (basato su ultime N puntate, esclude utente corrente)
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateAggressiveBidders(AuctionInfo auction, AppSettings settings, string currentUsername)
|
||||||
|
{
|
||||||
|
// ?? FIX: Usa finestra scorrevole di ultime N puntate
|
||||||
|
var windowSize = settings.AggressiveBidderWindowSize > 0 ? settings.AggressiveBidderWindowSize : 30;
|
||||||
|
var recentWindow = auction.RecentBids
|
||||||
|
.Take(windowSize)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var bidCounts = recentWindow
|
||||||
|
.GroupBy(b => b.Username, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(g => new { Username = g.Key, Count = g.Count(), Percentage = (double)g.Count() / recentWindow.Count * 100 })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
auction.AggressiveBidders.Clear();
|
||||||
|
|
||||||
|
foreach (var bidder in bidCounts)
|
||||||
|
{
|
||||||
|
// ?? FIX: NON aggiungere l'utente corrente come aggressivo!
|
||||||
|
if (bidder.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// ?? FIX: Soglia più permissiva - usa percentuale invece di conteggio assoluto
|
||||||
|
// Un bidder è "aggressivo" se ha più del 40% delle puntate nella finestra (configurabile)
|
||||||
|
var percentageThreshold = settings.AggressiveBidderPercentageThreshold > 0 ? settings.AggressiveBidderPercentageThreshold : 40.0;
|
||||||
|
|
||||||
|
if (bidder.Percentage >= percentageThreshold || bidder.Count >= settings.AggressiveBidderThreshold)
|
||||||
|
{
|
||||||
|
auction.AggressiveBidders.Add(bidder.Username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rileva situazione di "duello" (solo 2 bidder attivi che si contendono l'asta)
|
||||||
|
/// In questa situazione bisogna essere pronti perché se uno si ritira l'altro vince
|
||||||
|
/// </summary>
|
||||||
|
private void DetectDuelSituation(AuctionInfo auction, AppSettings settings, string currentUsername)
|
||||||
|
{
|
||||||
|
var windowSize = settings.DuelDetectionWindowSize > 0 ? settings.DuelDetectionWindowSize : 20;
|
||||||
|
var recentWindow = auction.RecentBids.Take(windowSize).ToList();
|
||||||
|
|
||||||
|
if (recentWindow.Count < 6) // Serve un minimo di puntate per rilevare un pattern
|
||||||
|
{
|
||||||
|
auction.IsDuelSituation = false;
|
||||||
|
auction.DuelOpponent = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bidders = recentWindow
|
||||||
|
.GroupBy(b => b.Username, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(g => new { Username = g.Key, Count = g.Count(), Percentage = (double)g.Count() / recentWindow.Count * 100 })
|
||||||
|
.OrderByDescending(b => b.Count)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Duello: esattamente 2 bidder dominanti che coprono almeno l'80% delle puntate
|
||||||
|
if (bidders.Count >= 2)
|
||||||
|
{
|
||||||
|
var top2Percentage = bidders.Take(2).Sum(b => b.Percentage);
|
||||||
|
|
||||||
|
if (top2Percentage >= 80 && bidders.Count <= 3)
|
||||||
|
{
|
||||||
|
auction.IsDuelSituation = true;
|
||||||
|
|
||||||
|
// Trova l'avversario (chi NON sono io)
|
||||||
|
var opponent = bidders.FirstOrDefault(b =>
|
||||||
|
!b.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
auction.DuelOpponent = opponent?.Username;
|
||||||
|
|
||||||
|
// Calcola chi sta dominando
|
||||||
|
var myStats = bidders.FirstOrDefault(b =>
|
||||||
|
b.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
auction.DuelAdvantage = myStats != null && opponent != null
|
||||||
|
? myStats.Percentage - opponent.Percentage
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auction.IsDuelSituation = false;
|
||||||
|
auction.DuelOpponent = null;
|
||||||
|
auction.DuelAdvantage = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auction.IsDuelSituation = false;
|
||||||
|
auction.DuelOpponent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifica se è il caso di puntare considerando tutte le strategie
|
||||||
|
/// </summary>
|
||||||
|
public BidDecision ShouldPlaceBid(AuctionInfo auction, AuctionState state, AppSettings settings, string currentUsername)
|
||||||
|
{
|
||||||
|
var decision = new BidDecision { ShouldBid = true };
|
||||||
|
|
||||||
|
// Se le strategie avanzate sono disabilitate per questa asta, salta tutto
|
||||||
|
if (auction.AdvancedStrategiesEnabled == false)
|
||||||
|
{
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Verifica soft retreat
|
||||||
|
if (settings.SoftRetreatEnabled || (auction.SoftRetreatEnabledOverride ?? settings.SoftRetreatEnabled))
|
||||||
|
{
|
||||||
|
if (auction.IsInSoftRetreat)
|
||||||
|
{
|
||||||
|
var retreatEnd = auction.LastSoftRetreatAt?.AddSeconds(settings.SoftRetreatDurationSeconds);
|
||||||
|
if (retreatEnd > DateTime.UtcNow)
|
||||||
|
{
|
||||||
|
decision.ShouldBid = false;
|
||||||
|
decision.Reason = $"Soft retreat attivo (termina tra {(retreatEnd.Value - DateTime.UtcNow).TotalSeconds:F0}s)";
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fine soft retreat
|
||||||
|
auction.IsInSoftRetreat = false;
|
||||||
|
auction.ConsecutiveCollisions = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica se attivare soft retreat
|
||||||
|
if (auction.ConsecutiveCollisions >= settings.SoftRetreatAfterCollisions)
|
||||||
|
{
|
||||||
|
auction.IsInSoftRetreat = true;
|
||||||
|
auction.LastSoftRetreatAt = DateTime.UtcNow;
|
||||||
|
decision.ShouldBid = false;
|
||||||
|
decision.Reason = $"Soft retreat attivato dopo {auction.ConsecutiveCollisions} collisioni";
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Verifica competition threshold
|
||||||
|
if (settings.CompetitionDetectionEnabled)
|
||||||
|
{
|
||||||
|
if (auction.ActiveBiddersCount >= settings.CompetitionThreshold)
|
||||||
|
{
|
||||||
|
// Controlla se l'ultimo bidder sono io - se sì, posso continuare
|
||||||
|
var lastBid = auction.RecentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
|
||||||
|
if (lastBid != null && !lastBid.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (settings.AutoPauseHotAuctions && auction.HeatMetric >= settings.HeatThresholdForPause)
|
||||||
|
{
|
||||||
|
decision.ShouldBid = false;
|
||||||
|
decision.Reason = $"Asta troppo calda (heat={auction.HeatMetric}%, bidder={auction.ActiveBiddersCount})";
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Verifica opponent profiling
|
||||||
|
if (settings.OpponentProfilingEnabled && auction.AggressiveBidders.Count > 0)
|
||||||
|
{
|
||||||
|
if (settings.AggressiveBidderAction == "Avoid")
|
||||||
|
{
|
||||||
|
decision.ShouldBid = false;
|
||||||
|
decision.Reason = $"Bidder aggressivi rilevati: {string.Join(", ", auction.AggressiveBidders.Take(3))}";
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Probabilistic bidding
|
||||||
|
if (settings.ProbabilisticBiddingEnabled)
|
||||||
|
{
|
||||||
|
var probability = CalculateBidProbability(auction, settings);
|
||||||
|
var roll = _random.NextDouble();
|
||||||
|
|
||||||
|
if (roll > probability)
|
||||||
|
{
|
||||||
|
decision.ShouldBid = false;
|
||||||
|
decision.Reason = $"Skip probabilistico (p={probability:P0}, roll={roll:P0})";
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Bankroll manager
|
||||||
|
if (settings.BankrollManagerEnabled)
|
||||||
|
{
|
||||||
|
var bankrollCheck = CheckBankrollLimits(auction, settings);
|
||||||
|
if (!bankrollCheck.CanBid)
|
||||||
|
{
|
||||||
|
decision.ShouldBid = false;
|
||||||
|
decision.Reason = bankrollCheck.Reason;
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcola probabilità di puntata basata su competizione e ROI
|
||||||
|
/// </summary>
|
||||||
|
private double CalculateBidProbability(AuctionInfo auction, AppSettings settings)
|
||||||
|
{
|
||||||
|
var probability = settings.BaseBidProbability;
|
||||||
|
|
||||||
|
// Riduci probabilità per ogni bidder attivo oltre la soglia
|
||||||
|
var extraBidders = Math.Max(0, auction.ActiveBiddersCount - settings.CompetitionThreshold);
|
||||||
|
probability -= extraBidders * settings.ProbabilityReductionPerBidder;
|
||||||
|
|
||||||
|
// Riduci per heat metric alto
|
||||||
|
if (auction.HeatMetric > 70)
|
||||||
|
{
|
||||||
|
probability -= 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aumenta se abbiamo un buon ROI potenziale
|
||||||
|
if (auction.CalculatedValue?.Savings > 0)
|
||||||
|
{
|
||||||
|
probability += 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Clamp(probability, 0.1, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifica limiti bankroll
|
||||||
|
/// </summary>
|
||||||
|
private BankrollCheckResult CheckBankrollLimits(AuctionInfo auction, AppSettings settings)
|
||||||
|
{
|
||||||
|
var result = new BankrollCheckResult { CanBid = true };
|
||||||
|
|
||||||
|
// Limite puntate per asta
|
||||||
|
var maxPerAuction = auction.MaxBidsOverride ?? settings.MaxBidsPerAuction;
|
||||||
|
if (maxPerAuction > 0 && auction.SessionBidCount >= maxPerAuction)
|
||||||
|
{
|
||||||
|
result.CanBid = false;
|
||||||
|
result.Reason = $"Limite puntate per asta raggiunto ({auction.SessionBidCount}/{maxPerAuction})";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limite puntate per sessione
|
||||||
|
if (settings.MaxBidsPerSession > 0 && _sessionTotalBids >= settings.MaxBidsPerSession)
|
||||||
|
{
|
||||||
|
result.CanBid = false;
|
||||||
|
result.Reason = $"Limite puntate per sessione raggiunto ({_sessionTotalBids}/{settings.MaxBidsPerSession})";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Budget giornaliero
|
||||||
|
if (settings.DailyBudgetEuro > 0)
|
||||||
|
{
|
||||||
|
var spent = _sessionTotalBids * settings.AverageBidCostEuro;
|
||||||
|
if (spent >= settings.DailyBudgetEuro)
|
||||||
|
{
|
||||||
|
result.CanBid = false;
|
||||||
|
result.Reason = $"Budget giornaliero esaurito (€{spent:F2}/€{settings.DailyBudgetEuro:F2})";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registra una puntata effettuata (per tracking)
|
||||||
|
/// </summary>
|
||||||
|
public void RecordBidAttempt(AuctionInfo auction, bool success, bool collision = false)
|
||||||
|
{
|
||||||
|
auction.SessionBidCount++;
|
||||||
|
_sessionTotalBids++;
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
auction.SuccessfulBidCount++;
|
||||||
|
auction.ConsecutiveCollisions = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auction.FailedBidCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collision)
|
||||||
|
{
|
||||||
|
auction.CollisionCount++;
|
||||||
|
auction.ConsecutiveCollisions++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registra un timer scaduto
|
||||||
|
/// </summary>
|
||||||
|
public void RecordTimerExpired(AuctionInfo auction)
|
||||||
|
{
|
||||||
|
auction.TimerExpiredCount++;
|
||||||
|
auction.ConsecutiveCollisions++; // Conta come "mancato"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reset contatori sessione
|
||||||
|
/// </summary>
|
||||||
|
public void ResetSession()
|
||||||
|
{
|
||||||
|
_sessionTotalBids = 0;
|
||||||
|
_sessionStartedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ottiene statistiche sessione corrente
|
||||||
|
/// </summary>
|
||||||
|
public SessionStats GetSessionStats()
|
||||||
|
{
|
||||||
|
return new SessionStats
|
||||||
|
{
|
||||||
|
TotalBids = _sessionTotalBids,
|
||||||
|
SessionDuration = DateTime.UtcNow - _sessionStartedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Risultato calcolo timing puntata
|
||||||
|
/// </summary>
|
||||||
|
public class BidTimingResult
|
||||||
|
{
|
||||||
|
public int BaseOffsetMs { get; set; }
|
||||||
|
public int LatencyCompensationMs { get; set; }
|
||||||
|
public int DynamicAdjustmentMs { get; set; }
|
||||||
|
public int JitterMs { get; set; }
|
||||||
|
public int FinalOffsetMs { get; set; }
|
||||||
|
public bool ShouldBid { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decisione se puntare
|
||||||
|
/// </summary>
|
||||||
|
public class BidDecision
|
||||||
|
{
|
||||||
|
public bool ShouldBid { get; set; }
|
||||||
|
public string? Reason { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Risultato verifica bankroll
|
||||||
|
/// </summary>
|
||||||
|
public class BankrollCheckResult
|
||||||
|
{
|
||||||
|
public bool CanBid { get; set; }
|
||||||
|
public string? Reason { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statistiche sessione
|
||||||
|
/// </summary>
|
||||||
|
public class SessionStats
|
||||||
|
{
|
||||||
|
public int TotalBids { get; set; }
|
||||||
|
public TimeSpan SessionDuration { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -375,7 +375,13 @@ namespace AutoBidder.Services
|
|||||||
var nameMatch = Regex.Match(html, @"<a[^>]+class=""name[^""]*""[^>]*>([^<]+)</a>", RegexOptions.IgnoreCase);
|
var nameMatch = Regex.Match(html, @"<a[^>]+class=""name[^""]*""[^>]*>([^<]+)</a>", RegexOptions.IgnoreCase);
|
||||||
if (nameMatch.Success)
|
if (nameMatch.Success)
|
||||||
{
|
{
|
||||||
auction.Name = System.Net.WebUtility.HtmlDecode(nameMatch.Groups[1].Value.Trim());
|
var name = System.Net.WebUtility.HtmlDecode(nameMatch.Groups[1].Value.Trim());
|
||||||
|
// ?? FIX: Sostituisci entità HTML non standard
|
||||||
|
name = name
|
||||||
|
.Replace("+", "+")
|
||||||
|
.Replace("&plus;", "+")
|
||||||
|
.Replace(" + ", " & ");
|
||||||
|
auction.Name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estrai prezzo compralo subito
|
// Estrai prezzo compralo subito
|
||||||
|
|||||||
@@ -596,6 +596,142 @@ namespace AutoBidder.Services
|
|||||||
await using var cmd = conn.CreateCommand();
|
await using var cmd = conn.CreateCommand();
|
||||||
cmd.CommandText = sql;
|
cmd.CommandText = sql;
|
||||||
await cmd.ExecuteNonQueryAsync();
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}),
|
||||||
|
|
||||||
|
new Migration(12, "Add complete auction history with metrics", async (conn) => {
|
||||||
|
var sql = @"
|
||||||
|
-- Tabella storia completa aste con tutte le metriche
|
||||||
|
CREATE TABLE IF NOT EXISTS CompleteAuctionHistory (
|
||||||
|
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
AuctionId TEXT NOT NULL,
|
||||||
|
AuctionName TEXT NOT NULL,
|
||||||
|
ProductKey TEXT,
|
||||||
|
OriginalUrl TEXT,
|
||||||
|
|
||||||
|
-- Dati finali
|
||||||
|
FinalPrice REAL NOT NULL,
|
||||||
|
BuyNowPrice REAL,
|
||||||
|
ShippingCost REAL,
|
||||||
|
TotalCost REAL,
|
||||||
|
Savings REAL,
|
||||||
|
SavingsPercentage REAL,
|
||||||
|
|
||||||
|
-- Risultato
|
||||||
|
Won INTEGER NOT NULL DEFAULT 0,
|
||||||
|
WinnerUsername TEXT,
|
||||||
|
WinnerBidsUsed INTEGER,
|
||||||
|
|
||||||
|
-- Metriche competizione
|
||||||
|
TotalResets INTEGER DEFAULT 0,
|
||||||
|
TotalUniqueBidders INTEGER DEFAULT 0,
|
||||||
|
MaxHeatMetric INTEGER DEFAULT 0,
|
||||||
|
AvgHeatMetric REAL DEFAULT 0,
|
||||||
|
TotalCollisions INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
-- Mie statistiche
|
||||||
|
MyBidsUsed INTEGER DEFAULT 0,
|
||||||
|
MySuccessfulBids INTEGER DEFAULT 0,
|
||||||
|
MyFailedBids INTEGER DEFAULT 0,
|
||||||
|
MyTimerExpired INTEGER DEFAULT 0,
|
||||||
|
MyAvgLatencyMs REAL,
|
||||||
|
MyMinLatencyMs INTEGER,
|
||||||
|
MyMaxLatencyMs INTEGER,
|
||||||
|
|
||||||
|
-- Offset e timing
|
||||||
|
AvgOffsetUsedMs REAL,
|
||||||
|
FinalOffsetUsedMs INTEGER,
|
||||||
|
|
||||||
|
-- Bidder aggressivi rilevati (JSON array)
|
||||||
|
AggressiveBiddersJson TEXT,
|
||||||
|
|
||||||
|
-- Timeline puntate (JSON array per grafico)
|
||||||
|
BidTimelineJson TEXT,
|
||||||
|
|
||||||
|
-- Riepilogo puntatori (JSON)
|
||||||
|
BiddersSummaryJson TEXT,
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
TrackingStartedAt TEXT,
|
||||||
|
ClosedAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
ClosedAtHour INTEGER,
|
||||||
|
ClosedAtDayOfWeek INTEGER,
|
||||||
|
DurationSeconds INTEGER,
|
||||||
|
|
||||||
|
-- Flag
|
||||||
|
IsCompleteTracking INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
CreatedAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indici per query analytics
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cah_auctionid ON CompleteAuctionHistory(AuctionId);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cah_productkey ON CompleteAuctionHistory(ProductKey);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cah_won ON CompleteAuctionHistory(Won);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cah_closedat ON CompleteAuctionHistory(ClosedAt DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cah_hour ON CompleteAuctionHistory(ClosedAtHour);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cah_complete ON CompleteAuctionHistory(IsCompleteTracking);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cah_productkey_won ON CompleteAuctionHistory(ProductKey, Won);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cah_heat ON CompleteAuctionHistory(MaxHeatMetric);
|
||||||
|
";
|
||||||
|
await using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = sql;
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}),
|
||||||
|
|
||||||
|
new Migration(13, "Add bidder profiles table", async (conn) => {
|
||||||
|
var sql = @"
|
||||||
|
-- Tabella profili bidder (per opponent profiling)
|
||||||
|
CREATE TABLE IF NOT EXISTS BidderProfiles (
|
||||||
|
Username TEXT PRIMARY KEY,
|
||||||
|
TotalAuctionsParticipated INTEGER DEFAULT 0,
|
||||||
|
TotalBidsPlaced INTEGER DEFAULT 0,
|
||||||
|
AvgBidsPerAuction REAL DEFAULT 0,
|
||||||
|
WinRate REAL DEFAULT 0,
|
||||||
|
IsAggressive INTEGER DEFAULT 0,
|
||||||
|
IsBot INTEGER DEFAULT 0,
|
||||||
|
AvgResponseTimeMs REAL,
|
||||||
|
PreferredHoursJson TEXT,
|
||||||
|
LastSeenAt TEXT,
|
||||||
|
Notes TEXT,
|
||||||
|
CreatedAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UpdatedAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indici
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bidderprofiles_aggressive ON BidderProfiles(IsAggressive);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bidderprofiles_bot ON BidderProfiles(IsBot);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bidderprofiles_lastseen ON BidderProfiles(LastSeenAt DESC);
|
||||||
|
";
|
||||||
|
await using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = sql;
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}),
|
||||||
|
|
||||||
|
new Migration(14, "Add session metrics table", async (conn) => {
|
||||||
|
var sql = @"
|
||||||
|
-- Tabella metriche per sessione
|
||||||
|
CREATE TABLE IF NOT EXISTS SessionMetrics (
|
||||||
|
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
SessionStartedAt TEXT NOT NULL,
|
||||||
|
SessionEndedAt TEXT,
|
||||||
|
TotalBidsPlaced INTEGER DEFAULT 0,
|
||||||
|
TotalAuctionsWon INTEGER DEFAULT 0,
|
||||||
|
TotalAuctionsLost INTEGER DEFAULT 0,
|
||||||
|
TotalCollisions INTEGER DEFAULT 0,
|
||||||
|
TotalTimerExpired INTEGER DEFAULT 0,
|
||||||
|
AvgLatencyMs REAL,
|
||||||
|
BudgetUsedEuro REAL DEFAULT 0,
|
||||||
|
Notes TEXT,
|
||||||
|
CreatedAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indice
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sessionmetrics_started ON SessionMetrics(SessionStartedAt DESC);
|
||||||
|
";
|
||||||
|
await using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = sql;
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1088,6 +1224,177 @@ namespace AutoBidder.Services
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Salva storia completa di un'asta con tutte le metriche avanzate
|
||||||
|
/// Chiamato solo per aste tracciate dall'inizio
|
||||||
|
/// </summary>
|
||||||
|
public async Task SaveCompleteAuctionHistoryAsync(AuctionInfo auction, AuctionState finalState, bool won)
|
||||||
|
{
|
||||||
|
var closedAt = DateTime.UtcNow;
|
||||||
|
var productKey = ProductStatisticsService.GenerateProductKey(auction.Name);
|
||||||
|
|
||||||
|
// Prepara JSON per dati complessi
|
||||||
|
var aggressiveBiddersJson = auction.AggressiveBidders.Count > 0
|
||||||
|
? System.Text.Json.JsonSerializer.Serialize(auction.AggressiveBidders.ToList())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var biddersSummary = auction.BidderStats
|
||||||
|
.Select(bs => new { Username = bs.Key, BidCount = bs.Value.BidCount })
|
||||||
|
.OrderByDescending(b => b.BidCount)
|
||||||
|
.Take(20)
|
||||||
|
.ToList();
|
||||||
|
var biddersSummaryJson = System.Text.Json.JsonSerializer.Serialize(biddersSummary);
|
||||||
|
|
||||||
|
// Calcola durata se abbiamo il tracking start
|
||||||
|
int? durationSeconds = null;
|
||||||
|
if (auction.TrackingStartedAt.HasValue)
|
||||||
|
{
|
||||||
|
durationSeconds = (int)(closedAt - auction.TrackingStartedAt.Value).TotalSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
INSERT INTO CompleteAuctionHistory
|
||||||
|
(AuctionId, AuctionName, ProductKey, OriginalUrl,
|
||||||
|
FinalPrice, BuyNowPrice, ShippingCost, TotalCost, Savings, SavingsPercentage,
|
||||||
|
Won, WinnerUsername, WinnerBidsUsed,
|
||||||
|
TotalResets, TotalUniqueBidders, MaxHeatMetric, AvgHeatMetric, TotalCollisions,
|
||||||
|
MyBidsUsed, MySuccessfulBids, MyFailedBids, MyTimerExpired, MyAvgLatencyMs, MyMinLatencyMs, MyMaxLatencyMs,
|
||||||
|
AvgOffsetUsedMs, FinalOffsetUsedMs,
|
||||||
|
AggressiveBiddersJson, BiddersSummaryJson,
|
||||||
|
TrackingStartedAt, ClosedAt, ClosedAtHour, ClosedAtDayOfWeek, DurationSeconds,
|
||||||
|
IsCompleteTracking)
|
||||||
|
VALUES
|
||||||
|
(@auctionId, @auctionName, @productKey, @originalUrl,
|
||||||
|
@finalPrice, @buyNowPrice, @shippingCost, @totalCost, @savings, @savingsPercentage,
|
||||||
|
@won, @winnerUsername, @winnerBidsUsed,
|
||||||
|
@totalResets, @totalUniqueBidders, @maxHeatMetric, @avgHeatMetric, @totalCollisions,
|
||||||
|
@myBidsUsed, @mySuccessfulBids, @myFailedBids, @myTimerExpired, @myAvgLatencyMs, @myMinLatencyMs, @myMaxLatencyMs,
|
||||||
|
@avgOffsetUsedMs, @finalOffsetUsedMs,
|
||||||
|
@aggressiveBiddersJson, @biddersSummaryJson,
|
||||||
|
@trackingStartedAt, @closedAt, @closedAtHour, @closedAtDayOfWeek, @durationSeconds,
|
||||||
|
@isCompleteTracking);
|
||||||
|
";
|
||||||
|
|
||||||
|
await ExecuteNonQueryAsync(sql,
|
||||||
|
new SqliteParameter("@auctionId", auction.AuctionId),
|
||||||
|
new SqliteParameter("@auctionName", auction.Name),
|
||||||
|
new SqliteParameter("@productKey", productKey),
|
||||||
|
new SqliteParameter("@originalUrl", auction.OriginalUrl),
|
||||||
|
new SqliteParameter("@finalPrice", finalState.Price),
|
||||||
|
new SqliteParameter("@buyNowPrice", (object?)auction.BuyNowPrice ?? DBNull.Value),
|
||||||
|
new SqliteParameter("@shippingCost", (object?)auction.ShippingCost ?? DBNull.Value),
|
||||||
|
new SqliteParameter("@totalCost", (object?)auction.CalculatedValue?.TotalCostIfWin ?? DBNull.Value),
|
||||||
|
new SqliteParameter("@savings", (object?)auction.CalculatedValue?.Savings ?? DBNull.Value),
|
||||||
|
new SqliteParameter("@savingsPercentage", (object?)auction.CalculatedValue?.SavingsPercentage ?? DBNull.Value),
|
||||||
|
new SqliteParameter("@won", won ? 1 : 0),
|
||||||
|
new SqliteParameter("@winnerUsername", (object?)finalState.LastBidder ?? DBNull.Value),
|
||||||
|
new SqliteParameter("@winnerBidsUsed", DBNull.Value), // Da calcolare se disponibile
|
||||||
|
new SqliteParameter("@totalResets", auction.ResetCount),
|
||||||
|
new SqliteParameter("@totalUniqueBidders", auction.BidderStats.Count),
|
||||||
|
new SqliteParameter("@maxHeatMetric", auction.HeatMetric),
|
||||||
|
new SqliteParameter("@avgHeatMetric", (double)auction.HeatMetric),
|
||||||
|
new SqliteParameter("@totalCollisions", auction.CollisionCount),
|
||||||
|
new SqliteParameter("@myBidsUsed", auction.SessionBidCount),
|
||||||
|
new SqliteParameter("@mySuccessfulBids", auction.SuccessfulBidCount),
|
||||||
|
new SqliteParameter("@myFailedBids", auction.FailedBidCount),
|
||||||
|
new SqliteParameter("@myTimerExpired", auction.TimerExpiredCount),
|
||||||
|
new SqliteParameter("@myAvgLatencyMs", auction.AverageLatencyMs),
|
||||||
|
new SqliteParameter("@myMinLatencyMs", auction.LatencyHistory.Count > 0 ? auction.LatencyHistory.Min() : DBNull.Value),
|
||||||
|
new SqliteParameter("@myMaxLatencyMs", auction.LatencyHistory.Count > 0 ? auction.LatencyHistory.Max() : (object)DBNull.Value),
|
||||||
|
new SqliteParameter("@avgOffsetUsedMs", (double)auction.DynamicOffsetMs),
|
||||||
|
new SqliteParameter("@finalOffsetUsedMs", auction.LastUsedOffsetMs),
|
||||||
|
new SqliteParameter("@aggressiveBiddersJson", (object?)aggressiveBiddersJson ?? DBNull.Value),
|
||||||
|
new SqliteParameter("@biddersSummaryJson", biddersSummaryJson),
|
||||||
|
new SqliteParameter("@trackingStartedAt", auction.TrackingStartedAt?.ToString("O") ?? (object)DBNull.Value),
|
||||||
|
new SqliteParameter("@closedAt", closedAt.ToString("O")),
|
||||||
|
new SqliteParameter("@closedAtHour", closedAt.Hour),
|
||||||
|
new SqliteParameter("@closedAtDayOfWeek", (int)closedAt.DayOfWeek),
|
||||||
|
new SqliteParameter("@durationSeconds", (object?)durationSeconds ?? DBNull.Value),
|
||||||
|
new SqliteParameter("@isCompleteTracking", auction.IsTrackedFromStart ? 1 : 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
Console.WriteLine($"[DatabaseService] ✓ Salvata storia completa per {auction.Name} (complete={auction.IsTrackedFromStart})");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ottiene la storia completa delle aste con filtri
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<CompleteAuctionHistoryRecord>> GetCompleteAuctionHistoryAsync(
|
||||||
|
string? productNameFilter = null,
|
||||||
|
bool? wonFilter = null,
|
||||||
|
int limit = 100,
|
||||||
|
string orderBy = "ClosedAt",
|
||||||
|
bool descending = true)
|
||||||
|
{
|
||||||
|
var results = new List<CompleteAuctionHistoryRecord>();
|
||||||
|
|
||||||
|
var sql = $@"
|
||||||
|
SELECT Id, AuctionId, AuctionName, ProductKey, OriginalUrl,
|
||||||
|
FinalPrice, BuyNowPrice, ShippingCost, TotalCost, Savings, SavingsPercentage,
|
||||||
|
Won, WinnerUsername, WinnerBidsUsed,
|
||||||
|
TotalResets, TotalUniqueBidders, MaxHeatMetric, AvgHeatMetric, TotalCollisions,
|
||||||
|
MyBidsUsed, MySuccessfulBids, MyFailedBids, MyTimerExpired, MyAvgLatencyMs,
|
||||||
|
ClosedAt, ClosedAtHour, DurationSeconds, IsCompleteTracking,
|
||||||
|
AggressiveBiddersJson, BiddersSummaryJson
|
||||||
|
FROM CompleteAuctionHistory
|
||||||
|
WHERE 1=1
|
||||||
|
{(productNameFilter != null ? "AND AuctionName LIKE @nameFilter" : "")}
|
||||||
|
{(wonFilter.HasValue ? "AND Won = @wonFilter" : "")}
|
||||||
|
ORDER BY {orderBy} {(descending ? "DESC" : "ASC")}
|
||||||
|
LIMIT @limit;
|
||||||
|
";
|
||||||
|
|
||||||
|
await using var connection = await GetConnectionAsync();
|
||||||
|
await using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = sql;
|
||||||
|
cmd.Parameters.AddWithValue("@limit", limit);
|
||||||
|
|
||||||
|
if (productNameFilter != null)
|
||||||
|
cmd.Parameters.AddWithValue("@nameFilter", $"%{productNameFilter}%");
|
||||||
|
if (wonFilter.HasValue)
|
||||||
|
cmd.Parameters.AddWithValue("@wonFilter", wonFilter.Value ? 1 : 0);
|
||||||
|
|
||||||
|
await using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
results.Add(new CompleteAuctionHistoryRecord
|
||||||
|
{
|
||||||
|
Id = reader.GetInt32(0),
|
||||||
|
AuctionId = reader.GetString(1),
|
||||||
|
AuctionName = reader.GetString(2),
|
||||||
|
ProductKey = reader.IsDBNull(3) ? null : reader.GetString(3),
|
||||||
|
OriginalUrl = reader.IsDBNull(4) ? null : reader.GetString(4),
|
||||||
|
FinalPrice = reader.GetDouble(5),
|
||||||
|
BuyNowPrice = reader.IsDBNull(6) ? null : reader.GetDouble(6),
|
||||||
|
ShippingCost = reader.IsDBNull(7) ? null : reader.GetDouble(7),
|
||||||
|
TotalCost = reader.IsDBNull(8) ? null : reader.GetDouble(8),
|
||||||
|
Savings = reader.IsDBNull(9) ? null : reader.GetDouble(9),
|
||||||
|
SavingsPercentage = reader.IsDBNull(10) ? null : reader.GetDouble(10),
|
||||||
|
Won = reader.GetInt32(11) == 1,
|
||||||
|
WinnerUsername = reader.IsDBNull(12) ? null : reader.GetString(12),
|
||||||
|
WinnerBidsUsed = reader.IsDBNull(13) ? null : reader.GetInt32(13),
|
||||||
|
TotalResets = reader.IsDBNull(14) ? 0 : reader.GetInt32(14),
|
||||||
|
TotalUniqueBidders = reader.IsDBNull(15) ? 0 : reader.GetInt32(15),
|
||||||
|
MaxHeatMetric = reader.IsDBNull(16) ? 0 : reader.GetInt32(16),
|
||||||
|
AvgHeatMetric = reader.IsDBNull(17) ? 0 : reader.GetDouble(17),
|
||||||
|
TotalCollisions = reader.IsDBNull(18) ? 0 : reader.GetInt32(18),
|
||||||
|
MyBidsUsed = reader.IsDBNull(19) ? 0 : reader.GetInt32(19),
|
||||||
|
MySuccessfulBids = reader.IsDBNull(20) ? 0 : reader.GetInt32(20),
|
||||||
|
MyFailedBids = reader.IsDBNull(21) ? 0 : reader.GetInt32(21),
|
||||||
|
MyTimerExpired = reader.IsDBNull(22) ? 0 : reader.GetInt32(22),
|
||||||
|
MyAvgLatencyMs = reader.IsDBNull(23) ? null : reader.GetDouble(23),
|
||||||
|
ClosedAt = reader.IsDBNull(24) ? DateTime.MinValue : DateTime.Parse(reader.GetString(24)),
|
||||||
|
ClosedAtHour = reader.IsDBNull(25) ? 0 : reader.GetInt32(25),
|
||||||
|
DurationSeconds = reader.IsDBNull(26) ? null : reader.GetInt32(26),
|
||||||
|
IsCompleteTracking = reader.GetInt32(27) == 1,
|
||||||
|
AggressiveBiddersJson = reader.IsDBNull(28) ? null : reader.GetString(28),
|
||||||
|
BiddersSummaryJson = reader.IsDBNull(29) ? null : reader.GetString(29)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Aggiorna o inserisce statistiche aggregate per un prodotto
|
/// Aggiorna o inserisce statistiche aggregate per un prodotto
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,410 +0,0 @@
|
|||||||
# ?? GUIDA CONFIGURAZIONE UNRAID - AutoBidder v1.2.0
|
|
||||||
|
|
||||||
## ?? Template Container Unraid
|
|
||||||
|
|
||||||
### Informazioni Base
|
|
||||||
|
|
||||||
```
|
|
||||||
Nome: AutoBidder
|
|
||||||
Descrizione: Sistema automatizzato gestione aste Bidoo
|
|
||||||
Repository: gitea.encke-hake.ts.net/alby96/autobidder:1.2.0
|
|
||||||
WebUI: http://[IP]:[PORT:8889]
|
|
||||||
Icon URL: (opzionale)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Configurazione Parametri
|
|
||||||
|
|
||||||
### 1. Port Mappings
|
|
||||||
|
|
||||||
| Nome | Container Port | Host Port | Tipo | Descrizione |
|
|
||||||
|------|---------------|-----------|------|-------------|
|
|
||||||
| **WebUI** | `8080` | `8889` | TCP | Interfaccia web AutoBidder |
|
|
||||||
|
|
||||||
**Configurazione Unraid:**
|
|
||||||
```
|
|
||||||
Container Port: 8080
|
|
||||||
Host Port: 8889
|
|
||||||
Connection Type: TCP
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Volume Mappings
|
|
||||||
|
|
||||||
| Nome | Container Path | Host Path | Modo | Descrizione |
|
|
||||||
|------|---------------|-----------|------|-------------|
|
|
||||||
| **AppData** | `/app/Data` | `/mnt/user/appdata/autobidder/data` | Read/Write | Database e configurazioni |
|
|
||||||
| **Logs** | `/app/logs` | `/mnt/user/appdata/autobidder/logs` | Read/Write | Log applicazione (opzionale) |
|
|
||||||
|
|
||||||
**Configurazione Unraid:**
|
|
||||||
```
|
|
||||||
Volume 1:
|
|
||||||
Container Path: /app/Data
|
|
||||||
Host Path: /mnt/user/appdata/autobidder/data
|
|
||||||
Access Mode: Read/Write
|
|
||||||
|
|
||||||
Volume 2 (opzionale):
|
|
||||||
Container Path: /app/logs
|
|
||||||
Host Path: /mnt/user/appdata/autobidder/logs
|
|
||||||
Access Mode: Read/Write
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Environment Variables (OBBLIGATORIO)
|
|
||||||
|
|
||||||
#### ?? Autenticazione Applicazione
|
|
||||||
|
|
||||||
| Variable | Valore | Descrizione |
|
|
||||||
|----------|--------|-------------|
|
|
||||||
| **ADMIN_USERNAME** | `admin` | Username amministratore |
|
|
||||||
| **ADMIN_PASSWORD** | `MyS3cur3P@ss!2024` | Password admin (min 12 caratteri) |
|
|
||||||
|
|
||||||
**Requisiti password:**
|
|
||||||
- ? Minimo 12 caratteri
|
|
||||||
- ? Maiuscole + minuscole
|
|
||||||
- ? Numeri
|
|
||||||
- ? Simboli speciali
|
|
||||||
|
|
||||||
#### ?? Sessione Bidoo
|
|
||||||
|
|
||||||
**NON servono credenziali qui!**
|
|
||||||
|
|
||||||
Il cookie di sessione Bidoo si configura **dall'interfaccia web**:
|
|
||||||
1. Login su AutoBidder
|
|
||||||
2. Vai su **Settings ? Sessione Bidoo**
|
|
||||||
3. Incolla il cookie di sessione ottenuto da Bidoo.it
|
|
||||||
4. Salva
|
|
||||||
|
|
||||||
#### ?? Opzionali
|
|
||||||
|
|
||||||
| Variable | Valore Default | Descrizione |
|
|
||||||
|----------|---------------|-------------|
|
|
||||||
| **ASPNETCORE_ENVIRONMENT** | `Production` | Ambiente ASP.NET |
|
|
||||||
| **USE_POSTGRES** | `true` | Usa PostgreSQL per stats |
|
|
||||||
| **LOG_LEVEL** | `Information` | Livello logging |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Template Completo Unraid
|
|
||||||
|
|
||||||
### XML Template (my-AutoBidder.xml)
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<?xml version="1.0"?>
|
|
||||||
<Container version="2">
|
|
||||||
<Name>AutoBidder</Name>
|
|
||||||
<Repository>gitea.encke-hake.ts.net/alby96/autobidder:1.2.0</Repository>
|
|
||||||
<Registry>https://gitea.encke-hake.ts.net/</Registry>
|
|
||||||
<Network>bridge</Network>
|
|
||||||
<MyIP/>
|
|
||||||
<Shell>sh</Shell>
|
|
||||||
<Privileged>false</Privileged>
|
|
||||||
<Support>https://gitea.encke-hake.ts.net/Alby96/Mimante</Support>
|
|
||||||
<Project>https://gitea.encke-hake.ts.net/Alby96/Mimante</Project>
|
|
||||||
<Overview>Sistema Blazor .NET 8 per monitoraggio e partecipazione automatica aste Bidoo</Overview>
|
|
||||||
<Category>Tools:</Category>
|
|
||||||
<WebUI>http://[IP]:[PORT:8889]</WebUI>
|
|
||||||
<TemplateURL/>
|
|
||||||
<Icon>https://raw.githubusercontent.com/selfhosters/unRAID-CA-templates/master/templates/img/bidoo.png</Icon>
|
|
||||||
<ExtraParams/>
|
|
||||||
<PostArgs/>
|
|
||||||
<CPUset/>
|
|
||||||
<DateInstalled></DateInstalled>
|
|
||||||
<DonateText/>
|
|
||||||
<DonateLink/>
|
|
||||||
<Requires/>
|
|
||||||
|
|
||||||
<Config Name="WebUI Port" Target="8080" Default="8889" Mode="tcp" Description="Porta interfaccia web" Type="Port" Display="always" Required="true" Mask="false">8889</Config>
|
|
||||||
|
|
||||||
<Config Name="AppData" Target="/app/Data" Default="/mnt/user/appdata/autobidder/data" Mode="rw" Description="Database e configurazioni persistenti" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/autobidder/data</Config>
|
|
||||||
|
|
||||||
<Config Name="Logs" Target="/app/logs" Default="/mnt/user/appdata/autobidder/logs" Mode="rw" Description="Log applicazione (opzionale)" Type="Path" Display="advanced" Required="false" Mask="false">/mnt/user/appdata/autobidder/logs</Config>
|
|
||||||
|
|
||||||
<Config Name="Admin Username" Target="ADMIN_USERNAME" Default="admin" Mode="" Description="Username amministratore AutoBidder" Type="Variable" Display="always" Required="true" Mask="false">admin</Config>
|
|
||||||
|
|
||||||
<Config Name="Admin Password" Target="ADMIN_PASSWORD" Default="" Mode="" Description="Password amministratore (min 12 caratteri, maiuscole, minuscole, numeri, simboli)" Type="Variable" Display="always" Required="true" Mask="true"></Config>
|
|
||||||
|
|
||||||
<Config Name="Environment" Target="ASPNETCORE_ENVIRONMENT" Default="Production" Mode="" Description="Ambiente ASP.NET" Type="Variable" Display="advanced" Required="false" Mask="false">Production</Config>
|
|
||||||
|
|
||||||
<Config Name="Use PostgreSQL" Target="USE_POSTGRES" Default="true" Mode="" Description="Usa PostgreSQL per statistiche avanzate" Type="Variable" Display="advanced" Required="false" Mask="false">true</Config>
|
|
||||||
|
|
||||||
<Config Name="Log Level" Target="LOG_LEVEL" Default="Information" Mode="" Description="Livello logging (Debug, Information, Warning, Error)" Type="Variable" Display="advanced" Required="false" Mask="false">Information</Config>
|
|
||||||
</Container>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Installazione Step-by-Step
|
|
||||||
|
|
||||||
### Step 1: Aggiungi Container
|
|
||||||
|
|
||||||
1. Unraid WebUI ? **Docker** ? **Add Container**
|
|
||||||
2. Click: **Advanced View** (top right)
|
|
||||||
|
|
||||||
### Step 2: Configurazione Base
|
|
||||||
|
|
||||||
```
|
|
||||||
Name: AutoBidder
|
|
||||||
Repository: gitea.encke-hake.ts.net/alby96/autobidder:1.2.0
|
|
||||||
Network Type: Bridge
|
|
||||||
Console shell command: Shell
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Port Mappings
|
|
||||||
|
|
||||||
```
|
|
||||||
Container Port: 8080
|
|
||||||
Host Port: 8889
|
|
||||||
Protocol: TCP
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Path Mappings
|
|
||||||
|
|
||||||
```
|
|
||||||
Container Path: /app/Data
|
|
||||||
Host Path: /mnt/user/appdata/autobidder/data
|
|
||||||
Access Mode: Read/Write
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 5: Environment Variables
|
|
||||||
|
|
||||||
**OBBLIGATORIO - Autenticazione:**
|
|
||||||
```
|
|
||||||
Key: ADMIN_USERNAME
|
|
||||||
Value: admin
|
|
||||||
|
|
||||||
Key: ADMIN_PASSWORD
|
|
||||||
Value: TuaPasswordSicura123!
|
|
||||||
```
|
|
||||||
|
|
||||||
**Sessione Bidoo:**
|
|
||||||
```
|
|
||||||
NON configurare qui!
|
|
||||||
Si imposta dall'interfaccia web dopo il login.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Opzionali:**
|
|
||||||
```
|
|
||||||
Key: ASPNETCORE_ENVIRONMENT
|
|
||||||
Value: Production
|
|
||||||
|
|
||||||
Key: USE_POSTGRES
|
|
||||||
Value: true
|
|
||||||
|
|
||||||
Key: LOG_LEVEL
|
|
||||||
Value: Information
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 6: Apply e Start
|
|
||||||
|
|
||||||
1. Click **Apply**
|
|
||||||
2. Unraid scaricherà l'immagine
|
|
||||||
3. Container si avvierà automaticamente
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Verifica Installazione
|
|
||||||
|
|
||||||
### 1. Controlla Log
|
|
||||||
|
|
||||||
```
|
|
||||||
Unraid ? Docker ? AutoBidder ? Log
|
|
||||||
```
|
|
||||||
|
|
||||||
**Log attesi:**
|
|
||||||
```
|
|
||||||
[Identity] Database initialized
|
|
||||||
[Identity] Admin user created: admin
|
|
||||||
[DB] Database initialized successfully
|
|
||||||
[Kestrel] Listening on: http://+:8080
|
|
||||||
Application started
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Test WebUI
|
|
||||||
|
|
||||||
```
|
|
||||||
Browser: http://192.168.30.23:8889
|
|
||||||
```
|
|
||||||
|
|
||||||
Dovresti vedere:
|
|
||||||
- ? Redirect automatico a `/login`
|
|
||||||
- ? Pagina login AutoBidder
|
|
||||||
|
|
||||||
### 3. Primo Login
|
|
||||||
|
|
||||||
```
|
|
||||||
Username: admin
|
|
||||||
Password: (valore ADMIN_PASSWORD)
|
|
||||||
```
|
|
||||||
|
|
||||||
Dopo login:
|
|
||||||
- ? Homepage AutoBidder
|
|
||||||
- ? Monitoring aste attivo
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Troubleshooting
|
|
||||||
|
|
||||||
### Problema: Container non parte
|
|
||||||
|
|
||||||
**Verifica log:**
|
|
||||||
```
|
|
||||||
Unraid ? Docker ? AutoBidder ? Log
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cause comuni:**
|
|
||||||
- ? `ADMIN_PASSWORD` non configurata
|
|
||||||
- ? `BIDOO_USERNAME` o `BIDOO_PASSWORD` mancanti
|
|
||||||
- ? Port 8889 già in uso
|
|
||||||
|
|
||||||
**Soluzione:**
|
|
||||||
1. Stop container
|
|
||||||
2. Edit container
|
|
||||||
3. Verifica environment variables
|
|
||||||
4. Start container
|
|
||||||
|
|
||||||
### Problema: "Account temporaneamente bloccato"
|
|
||||||
|
|
||||||
**Causa:** 5 tentativi login falliti
|
|
||||||
|
|
||||||
**Soluzione:**
|
|
||||||
- Aspetta 15 minuti (lockout automatico)
|
|
||||||
- Verifica password configurata
|
|
||||||
|
|
||||||
### Problema: Pagina non carica
|
|
||||||
|
|
||||||
**Verifica:**
|
|
||||||
1. Container è "Started" (Unraid Docker)
|
|
||||||
2. Port 8889 corretto
|
|
||||||
3. IP Unraid corretto
|
|
||||||
|
|
||||||
**Test:**
|
|
||||||
```bash
|
|
||||||
# SSH su Unraid
|
|
||||||
curl http://localhost:8889
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problema: Bidoo non si connette
|
|
||||||
|
|
||||||
**Verifica:**
|
|
||||||
1. `BIDOO_USERNAME` e `BIDOO_PASSWORD` corretti
|
|
||||||
2. Account Bidoo attivo
|
|
||||||
3. Log container per errori connessione
|
|
||||||
|
|
||||||
**Log:**
|
|
||||||
```
|
|
||||||
Unraid ? Docker ? AutoBidder ? Log
|
|
||||||
Cerca: [Bidoo] o [Session]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Aggiornamento Versione
|
|
||||||
|
|
||||||
### Da v1.1.x a v1.2.0
|
|
||||||
|
|
||||||
1. **Stop container:**
|
|
||||||
```
|
|
||||||
Unraid ? Docker ? AutoBidder ? Stop
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Edit container:**
|
|
||||||
```
|
|
||||||
Unraid ? Docker ? AutoBidder ? Edit
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Aggiorna repository:**
|
|
||||||
```
|
|
||||||
Repository: gitea.encke-hake.ts.net/alby96/autobidder:1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Aggiungi nuove env vars:**
|
|
||||||
```
|
|
||||||
ADMIN_USERNAME=admin
|
|
||||||
ADMIN_PASSWORD=TuaPasswordSicura123!
|
|
||||||
BIDOO_USERNAME=email@bidoo.com
|
|
||||||
BIDOO_PASSWORD=bidoo_pass
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Apply e Start**
|
|
||||||
|
|
||||||
6. **Verifica log** (primo avvio)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Checklist Configurazione
|
|
||||||
|
|
||||||
Prima di avviare container:
|
|
||||||
|
|
||||||
- [ ] Repository corretto (`1.2.0`)
|
|
||||||
- [ ] Port mapping: `8889:8080`
|
|
||||||
- [ ] Volume: `/app/Data` ? `/mnt/user/appdata/autobidder/data`
|
|
||||||
- [ ] `ADMIN_USERNAME` configurato
|
|
||||||
- [ ] `ADMIN_PASSWORD` configurata (min 12 caratteri)
|
|
||||||
- [ ] `BIDOO_USERNAME` configurato
|
|
||||||
- [ ] `BIDOO_PASSWORD` configurata
|
|
||||||
- [ ] WebUI accessibile da browser
|
|
||||||
|
|
||||||
Dopo avvio:
|
|
||||||
|
|
||||||
- [ ] Log non mostra errori
|
|
||||||
- [ ] Login funzionante
|
|
||||||
- [ ] Homepage AutoBidder carica
|
|
||||||
- [ ] Connessione Bidoo OK
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Esempio Configurazione Completa
|
|
||||||
|
|
||||||
```
|
|
||||||
=== CONTAINER SETTINGS ===
|
|
||||||
Name: AutoBidder
|
|
||||||
Repository: gitea.encke-hake.ts.net/alby96/autobidder:1.2.0
|
|
||||||
Network: bridge
|
|
||||||
|
|
||||||
=== PORT MAPPINGS ===
|
|
||||||
8080 (container) ? 8889 (host) [TCP]
|
|
||||||
|
|
||||||
=== VOLUME MAPPINGS ===
|
|
||||||
/app/Data ? /mnt/user/appdata/autobidder/data [RW]
|
|
||||||
/app/logs ? /mnt/user/appdata/autobidder/logs [RW]
|
|
||||||
|
|
||||||
=== ENVIRONMENT VARIABLES ===
|
|
||||||
ADMIN_USERNAME=admin
|
|
||||||
ADMIN_PASSWORD=MyS3cur3P@ssw0rd!2024
|
|
||||||
ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
USE_POSTGRES=true
|
|
||||||
LOG_LEVEL=Information
|
|
||||||
|
|
||||||
=== SESSIONE BIDOO ===
|
|
||||||
Configurata dall'interfaccia web:
|
|
||||||
Settings ? Sessione Bidoo ? Incolla cookie
|
|
||||||
|
|
||||||
=== ACCESS ===
|
|
||||||
WebUI: http://192.168.30.23:8889
|
|
||||||
Login: admin / MyS3cur3P@ssw0rd!2024
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Supporto
|
|
||||||
|
|
||||||
**Documentazione:**
|
|
||||||
- [SECURITY.md](../SECURITY.md) - Guida sicurezza
|
|
||||||
- [README.md](../README.md) - Overview progetto
|
|
||||||
- [CHANGELOG.md](../CHANGELOG.md) - Note versioni
|
|
||||||
|
|
||||||
**Log dettagliati:**
|
|
||||||
```
|
|
||||||
Unraid ? Docker ? AutoBidder ? Log
|
|
||||||
```
|
|
||||||
|
|
||||||
**Issues:**
|
|
||||||
https://gitea.encke-hake.ts.net/Alby96/Mimante/issues
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**?? AutoBidder v1.2.0 - Pronto per Unraid con autenticazione sicura!**
|
|
||||||
@@ -99,6 +99,250 @@ namespace AutoBidder.Utilities
|
|||||||
/// Default: 180 (6 mesi), 0 = disabilitato
|
/// Default: 180 (6 mesi), 0 = disabilitato
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int DatabaseMaxRetentionDays { get; set; } = 180;
|
public int DatabaseMaxRetentionDays { get; set; } = 180;
|
||||||
|
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
// STRATEGIE AVANZATE DI PUNTATA
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abilita compensazione adattiva della latenza.
|
||||||
|
/// Misura latenza reale per ogni asta e adatta l'anticipo automaticamente.
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool AdaptiveLatencyEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abilita jitter casuale sull'offset per evitare sincronizzazione con altri bot.
|
||||||
|
/// Aggiunge ±JitterRangeMs al timing di puntata.
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool JitterEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Range massimo del jitter casuale in millisecondi (±X ms).
|
||||||
|
/// Default: 50 (range -50ms a +50ms)
|
||||||
|
/// </summary>
|
||||||
|
public int JitterRangeMs { get; set; } = 50;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abilita offset dinamico per asta basato su ping, storico e volatilità.
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool DynamicOffsetEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offset minimo garantito in ms (non scende mai sotto questo valore).
|
||||||
|
/// Default: 80
|
||||||
|
/// </summary>
|
||||||
|
public int MinimumOffsetMs { get; set; } = 80;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offset massimo in ms (non supera mai questo valore).
|
||||||
|
/// Default: 500
|
||||||
|
/// </summary>
|
||||||
|
public int MaximumOffsetMs { get; set; } = 500;
|
||||||
|
|
||||||
|
// ?? STRATEGIA ANTI-AUTOBID BIDOO
|
||||||
|
// Bidoo ha un sistema di auto-puntata integrato che si attiva a ~2 secondi.
|
||||||
|
// Aspettando che questa soglia venga superata, lasciamo che gli altri
|
||||||
|
// utenti con auto-puntata attiva puntino prima di noi, risparmiando puntate.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abilita la strategia di attesa per le auto-puntate di Bidoo.
|
||||||
|
/// Se true, aspetta che il timer scenda sotto la soglia prima di puntare.
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool WaitForAutoBidEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Soglia in secondi sotto la quale si può puntare.
|
||||||
|
/// Bidoo attiva le auto-puntate a ~2 secondi, quindi aspettiamo che passino.
|
||||||
|
/// Default: 1.8 (punta solo quando timer < 1.8s, dopo che le auto-puntate si sono attivate)
|
||||||
|
/// </summary>
|
||||||
|
public double WaitForAutoBidThresholdSeconds { get; set; } = 1.8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Se true, logga quando salta una puntata per aspettare le auto-puntate.
|
||||||
|
/// Default: false (per evitare spam nel log)
|
||||||
|
/// </summary>
|
||||||
|
public bool LogAutoBidWaitSkips { get; set; } = false;
|
||||||
|
|
||||||
|
// ??????????????????????????????????????????????????????????????
|
||||||
|
// RILEVAMENTO COMPETIZIONE E HEAT METRIC
|
||||||
|
// ??????????????????????????????????????????????????????????????
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abilita rilevamento competizione e heat metric.
|
||||||
|
/// Conta bidder attivi e collisioni per determinare il "calore" dell'asta.
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool CompetitionDetectionEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finestra temporale in secondi per contare bidder attivi.
|
||||||
|
/// Default: 30 (ultimi 30 secondi)
|
||||||
|
/// </summary>
|
||||||
|
public int CompetitionWindowSeconds { get; set; } = 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Numero minimo di bidder attivi per considerare l'asta "affollata".
|
||||||
|
/// Se >= a questa soglia, applica logica di evitamento.
|
||||||
|
/// Default: 3
|
||||||
|
/// </summary>
|
||||||
|
public int CompetitionThreshold { get; set; } = 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abilita auto-pausa per aste troppo competitive.
|
||||||
|
/// Default: false (solo warning, non pausa automatica)
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoPauseHotAuctions { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Soglia heat metric per auto-pausa (0-100).
|
||||||
|
/// Default: 80 (pausa se heat >= 80%)
|
||||||
|
/// </summary>
|
||||||
|
public int HeatThresholdForPause { get; set; } = 80;
|
||||||
|
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
// SOFT RETREAT E COLLISION MANAGEMENT
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abilita soft retreat automatico dopo N collisioni consecutive.
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool SoftRetreatEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Numero di collisioni consecutive per attivare soft retreat.
|
||||||
|
/// Default: 3
|
||||||
|
/// </summary>
|
||||||
|
public int SoftRetreatAfterCollisions { get; set; } = 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Durata pausa soft retreat in secondi.
|
||||||
|
/// Default: 30
|
||||||
|
/// </summary>
|
||||||
|
public int SoftRetreatDurationSeconds { get; set; } = 30;
|
||||||
|
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
// PROBABILISTIC BIDDING
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abilita policy di puntata probabilistica.
|
||||||
|
/// Decide se puntare con probabilità p basata su competizione e ROI.
|
||||||
|
/// Default: false (richiede tuning)
|
||||||
|
/// </summary>
|
||||||
|
public bool ProbabilisticBiddingEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Probabilità base di puntata (0.0 - 1.0).
|
||||||
|
/// Default: 0.8 (80%)
|
||||||
|
/// </summary>
|
||||||
|
public double BaseBidProbability { get; set; } = 0.8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fattore di riduzione probabilità per ogni bidder attivo extra.
|
||||||
|
/// Default: 0.1 (riduce del 10% per ogni bidder oltre la soglia)
|
||||||
|
/// </summary>
|
||||||
|
public double ProbabilityReductionPerBidder { get; set; } = 0.1;
|
||||||
|
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
// OPPONENT PROFILING
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abilita profiling degli avversari.
|
||||||
|
/// Identifica utenti aggressivi e applica regole specifiche.
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool OpponentProfilingEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Soglia puntate per considerare un utente "aggressivo".
|
||||||
|
/// Default: 10 (se un utente ha fatto >= 10 puntate in un'asta)
|
||||||
|
/// </summary>
|
||||||
|
public int AggressiveBidderThreshold { get; set; } = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dimensione finestra scorrevole per analisi bidder aggressivi.
|
||||||
|
/// Analizza le ultime N puntate invece del conteggio totale.
|
||||||
|
/// Default: 30 (ultime 30 puntate)
|
||||||
|
/// </summary>
|
||||||
|
public int AggressiveBidderWindowSize { get; set; } = 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Soglia percentuale per considerare un utente "aggressivo".
|
||||||
|
/// Se un utente ha più di X% delle puntate nella finestra, è aggressivo.
|
||||||
|
/// Default: 40 (40% delle puntate)
|
||||||
|
/// </summary>
|
||||||
|
public double AggressiveBidderPercentageThreshold { get; set; } = 40.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dimensione finestra per rilevamento situazioni di duello.
|
||||||
|
/// Default: 20 (ultime 20 puntate)
|
||||||
|
/// </summary>
|
||||||
|
public int DuelDetectionWindowSize { get; set; } = 20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Azione da intraprendere con bidder aggressivi.
|
||||||
|
/// "Avoid" = evita l'asta, "Compete" = continua normalmente, "Outbid" = punta più aggressivamente
|
||||||
|
/// Default: "Compete" (cambiato da Avoid per essere meno restrittivo)
|
||||||
|
/// </summary>
|
||||||
|
public string AggressiveBidderAction { get; set; } = "Compete";
|
||||||
|
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
// BANKROLL & SAFETY MANAGER
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abilita gestione bankroll per limitare spese.
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool BankrollManagerEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Limite massimo puntate per sessione (0 = illimitato).
|
||||||
|
/// Default: 0
|
||||||
|
/// </summary>
|
||||||
|
public int MaxBidsPerSession { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Limite massimo puntate per singola asta (0 = illimitato).
|
||||||
|
/// Default: 0
|
||||||
|
/// </summary>
|
||||||
|
public int MaxBidsPerAuction { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Budget massimo giornaliero in euro (0 = illimitato).
|
||||||
|
/// Calcolato come: puntate usate × costo medio puntata.
|
||||||
|
/// Default: 0
|
||||||
|
/// </summary>
|
||||||
|
public double DailyBudgetEuro { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Costo medio per puntata in euro (per calcolo budget).
|
||||||
|
/// Default: 0.15
|
||||||
|
/// </summary>
|
||||||
|
public double AverageBidCostEuro { get; set; } = 0.15;
|
||||||
|
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
// LOGGING AVANZATO
|
||||||
|
// ???????????????????????????????????????????????????????????????
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abilita logging avanzato con metriche dettagliate.
|
||||||
|
/// Include: collisioni, timer scaduto, latenza, heat metric.
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool AdvancedLoggingEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Salva metriche per ogni puntata nel database.
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool SaveBidMetricsToDatabase { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SettingsManager
|
public static class SettingsManager
|
||||||
|
|||||||
@@ -1,183 +0,0 @@
|
|||||||
# ? Verifica Configurazione Docker + Gitea (2026)
|
|
||||||
|
|
||||||
## ?? Checklist Completa secondo Guida Gitea
|
|
||||||
|
|
||||||
### ? 1. Preparazione su Gitea
|
|
||||||
|
|
||||||
| Requisito | Stato | Dettagli |
|
|
||||||
|-----------|-------|----------|
|
|
||||||
| Container Registry abilitato | ? CONFERMATO | Package esistente su `https://gitea.encke-hake.ts.net/Alby96/-/packages` |
|
|
||||||
| Token PAT generato | ?? DA VERIFICARE | Deve avere permessi `read:packages` + `write:packages` |
|
|
||||||
| Token usato per login | ?? DA FARE | `docker login gitea.encke-hake.ts.net` con Token come password |
|
|
||||||
|
|
||||||
**?? AZIONE RICHIESTA:**
|
|
||||||
```bash
|
|
||||||
# Genera token su: https://gitea.encke-hake.ts.net/user/settings/applications
|
|
||||||
# Scope necessari: read:packages, write:packages
|
|
||||||
|
|
||||||
# Poi autentica Docker:
|
|
||||||
docker login gitea.encke-hake.ts.net
|
|
||||||
# Username: Alby96
|
|
||||||
# Password: [TOKEN_PAT_GENERATO]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ? 2. Configurazione Progetto Visual Studio
|
|
||||||
|
|
||||||
| Requisito | Stato | Dettagli |
|
|
||||||
|-----------|-------|----------|
|
|
||||||
| Supporto Docker abilitato | ? OK | `DockerDefaultTargetOS=Linux`, `DockerfileFile=Dockerfile` |
|
|
||||||
| Dockerfile presente | ? OK | Valido, espone porta 8080, healthcheck configurato |
|
|
||||||
| .dockerignore presente | ? OK | Esclude file non necessari |
|
|
||||||
| Profili pubblicazione | ? OK | `GiteaRegistry.pubxml` e `GiteaRegistry-Versioned.pubxml` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ?? 3. Convenzione Nomi Docker (CORRETTO)
|
|
||||||
|
|
||||||
**? PROBLEMA RILEVATO E CORRETTO:**
|
|
||||||
|
|
||||||
**Prima (ERRATO - 4 livelli):**
|
|
||||||
```
|
|
||||||
gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
|
|
||||||
??????????????????????? ?????? ??????? ???????????
|
|
||||||
registro owner ??? ??? immagine
|
|
||||||
```
|
|
||||||
|
|
||||||
**Dopo correzione (CORRETTO - 3 livelli):**
|
|
||||||
```
|
|
||||||
gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
??????????????????????? ?????? ???????????
|
|
||||||
registro owner immagine
|
|
||||||
```
|
|
||||||
|
|
||||||
**?? Convenzione Gitea ufficiale:**
|
|
||||||
```
|
|
||||||
Sintassi: {registro}/{proprietario}/{immagine}:{tag}
|
|
||||||
Esempio: gitea.example.com/mio-utente/mia-app:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
**? MODIFICHE APPLICATE:**
|
|
||||||
- `AutoBidder.csproj`: `<ContainerRegistry>gitea.encke-hake.ts.net/alby96</ContainerRegistry>`
|
|
||||||
- `GiteaRegistry.pubxml`: Aggiornato con path corretto
|
|
||||||
- `DOCKER_PUBLISH_GUIDE.md`: Tutti i comandi aggiornati
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ? 4. File Modificati
|
|
||||||
|
|
||||||
#### `AutoBidder.csproj`
|
|
||||||
```xml
|
|
||||||
<!-- Metadata immagine Docker -->
|
|
||||||
<ContainerImageName>autobidder</ContainerImageName>
|
|
||||||
<ContainerImageTag>$(Version)</ContainerImageTag>
|
|
||||||
<!-- CORRETTO: Convenzione Gitea {registro}/{proprietario}/{immagine} -->
|
|
||||||
<ContainerRegistry>gitea.encke-hake.ts.net/alby96</ContainerRegistry>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `Properties/PublishProfiles/GiteaRegistry.pubxml`
|
|
||||||
```xml
|
|
||||||
<!-- CORRETTO: {registro}/{proprietario} senza livelli extra -->
|
|
||||||
<ContainerRegistry>gitea.encke-hake.ts.net/alby96</ContainerRegistry>
|
|
||||||
<ContainerImageName>autobidder</ContainerImageName>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `Dockerfile`
|
|
||||||
? **Nessuna modifica necessaria** - Il Dockerfile è corretto:
|
|
||||||
- Build multi-stage (sdk ? publish ? runtime)
|
|
||||||
- Porta 8080 esposta
|
|
||||||
- Healthcheck configurato
|
|
||||||
- Labels OCI
|
|
||||||
- Variabili ambiente corrette
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Procedura di Test
|
|
||||||
|
|
||||||
### 1. Autenticazione
|
|
||||||
```bash
|
|
||||||
docker logout gitea.encke-hake.ts.net
|
|
||||||
docker login gitea.encke-hake.ts.net
|
|
||||||
# Username: Alby96
|
|
||||||
# Password: [TOKEN_PAT]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Build con Convenzione Corretta
|
|
||||||
```bash
|
|
||||||
# Rebuild completo senza cache
|
|
||||||
docker build --no-cache \
|
|
||||||
-t gitea.encke-hake.ts.net/alby96/autobidder:latest \
|
|
||||||
-t gitea.encke-hake.ts.net/alby96/autobidder:1.0.0 \
|
|
||||||
.
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Push su Gitea
|
|
||||||
```bash
|
|
||||||
docker push gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
docker push gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Verifica su Gitea
|
|
||||||
```
|
|
||||||
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder/latest
|
|
||||||
```
|
|
||||||
|
|
||||||
Dovresti vedere:
|
|
||||||
- Nome package: `autobidder` (NON più `mimante/autobidder`)
|
|
||||||
- Tag disponibili: `latest`, `1.0.0`
|
|
||||||
- Data aggiornata ad oggi
|
|
||||||
- Digest SHA256 nuovo
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Confronto Prima/Dopo
|
|
||||||
|
|
||||||
| Aspetto | Prima (Errato) | Dopo (Corretto) |
|
|
||||||
|---------|----------------|-----------------|
|
|
||||||
| **Path Registry** | `gitea.encke-hake.ts.net/alby96/mimante` | `gitea.encke-hake.ts.net/alby96` |
|
|
||||||
| **Immagine Completa** | `gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest` | `gitea.encke-hake.ts.net/alby96/autobidder:latest` |
|
|
||||||
| **Package su Gitea** | `mimante/autobidder` | `autobidder` |
|
|
||||||
| **Link Gitea** | `.../container/mimante%2Fautobidder/...` | `.../container/autobidder/...` |
|
|
||||||
| **Livelli Path** | 4 (errato) | 3 (corretto) |
|
|
||||||
| **Conforme Guida Gitea** | ? NO | ? SÌ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Possibili Problemi e Soluzioni
|
|
||||||
|
|
||||||
### Problema 1: Package vecchio ancora visibile
|
|
||||||
**Soluzione:** Il vecchio package `mimante/autobidder` continuerà ad esistere. Puoi:
|
|
||||||
- Eliminarlo manualmente da Gitea (Settings del package)
|
|
||||||
- Oppure lasciarlo (non interferisce con il nuovo)
|
|
||||||
|
|
||||||
### Problema 2: Autenticazione fallita
|
|
||||||
**Soluzione:**
|
|
||||||
- Usa Token PAT invece della password
|
|
||||||
- Verifica scope del token: `read:packages`, `write:packages`
|
|
||||||
- Se hai 2FA attivo, il Token è OBBLIGATORIO
|
|
||||||
|
|
||||||
### Problema 3: SSL/TLS Errors
|
|
||||||
**Soluzione:** Se Gitea usa certificati self-signed:
|
|
||||||
```bash
|
|
||||||
# Aggiungi a Docker daemon.json
|
|
||||||
{
|
|
||||||
"insecure-registries": ["gitea.encke-hake.ts.net"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Configurazione Finale Verificata
|
|
||||||
|
|
||||||
**Tutti i requisiti soddisfatti:**
|
|
||||||
- ? Container Registry Gitea abilitato
|
|
||||||
- ? Dockerfile corretto e ottimizzato
|
|
||||||
- ? Convenzione nomi corretta (3 livelli)
|
|
||||||
- ? Profili di pubblicazione aggiornati
|
|
||||||
- ? Supporto Docker in Visual Studio
|
|
||||||
- ? Build multi-stage funzionante
|
|
||||||
- ? Healthcheck configurato
|
|
||||||
- ?? Token PAT da generare/verificare
|
|
||||||
|
|
||||||
**Prossimo step:** Genera Token PAT e testa il push!
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
# ?? Sistema di Versionamento Automatico
|
|
||||||
|
|
||||||
## ?? Strategia Versioning
|
|
||||||
|
|
||||||
Il progetto AutoBidder segue **[Semantic Versioning 2.0.0](https://semver.org/)** nel formato:
|
|
||||||
|
|
||||||
```
|
|
||||||
MAJOR.MINOR.PATCH
|
|
||||||
```
|
|
||||||
|
|
||||||
### Quando Incrementare
|
|
||||||
|
|
||||||
| Tipo | Quando | Esempio |
|
|
||||||
|------|--------|---------|
|
|
||||||
| **MAJOR** | Breaking changes | `1.5.2` ? `2.0.0` |
|
|
||||||
| **MINOR** | Nuove feature retrocompatibili | `1.5.2` ? `1.6.0` |
|
|
||||||
| **PATCH** | Bug fix retrocompatibili | `1.5.2` ? `1.5.3` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Workflow di Rilascio
|
|
||||||
|
|
||||||
### 1. Modifica Versione in `.csproj`
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- AutoBidder.csproj -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<!-- v1.1.0: Docker/Gitea publishing workflow + HTTPS fix -->
|
|
||||||
<Version>1.1.0</Version>
|
|
||||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
|
||||||
<FileVersion>1.1.0.0</FileVersion>
|
|
||||||
<InformationalVersion>1.1.0</InformationalVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
```
|
|
||||||
|
|
||||||
**? Questa è la FONTE UNICA della versione!**
|
|
||||||
|
|
||||||
### 2. Aggiorna `Dockerfile` Labels
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
LABEL org.opencontainers.image.version="1.1.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Documenta in `CHANGELOG.md`
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## [1.1.0] - 2025-01-18
|
|
||||||
|
|
||||||
### ? Aggiunte
|
|
||||||
- Pubblicazione automatica su Gitea
|
|
||||||
- ...
|
|
||||||
|
|
||||||
### ?? Modifiche
|
|
||||||
- Porta HTTP: 5000 ? 8080
|
|
||||||
- ...
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Pubblica su Gitea
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Da Visual Studio: Tasto destro ? Pubblica ? GiteaRegistry
|
|
||||||
# Oppure da CLI:
|
|
||||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
|
||||||
```
|
|
||||||
|
|
||||||
**Risultato automatico:**
|
|
||||||
- `gitea.../autobidder:latest` (aggiornato)
|
|
||||||
- `gitea.../autobidder:1.1.0` (nuovo tag)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Storico Versioni
|
|
||||||
|
|
||||||
### v1.1.0 - Docker/Gitea Publishing Workflow (2025-01-18)
|
|
||||||
|
|
||||||
**Feature Principali:**
|
|
||||||
- ? Pubblicazione automatica Gitea Container Registry
|
|
||||||
- ? Versionamento automatico da `.csproj`
|
|
||||||
- ?? HTTPS disabilitato di default in container
|
|
||||||
- ?? Porta HTTP standardizzata (8080)
|
|
||||||
- ?? Fix errore Visual Studio "ContainerBuild"
|
|
||||||
- ?? Fix crash container certificati HTTPS
|
|
||||||
|
|
||||||
**Breaking Changes:**
|
|
||||||
- ?? Porta: `5000` ? `8080`
|
|
||||||
- ?? Path Gitea: `alby96/mimante/autobidder` ? `alby96/autobidder`
|
|
||||||
- ?? HTTPS: abilitato ? disabilitato (opzionale)
|
|
||||||
|
|
||||||
**Migrazione:**
|
|
||||||
```bash
|
|
||||||
# Aggiorna port mapping
|
|
||||||
docker run -p 5000:8080 ... # era 5000:5000
|
|
||||||
|
|
||||||
# Pull nuova convenzione path
|
|
||||||
docker pull gitea.../alby96/autobidder:1.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### v1.0.0 - Release Iniziale (2025-01-17)
|
|
||||||
|
|
||||||
**Feature Principali:**
|
|
||||||
- ? Sistema AutoBidder Blazor .NET 8
|
|
||||||
- ? Monitoraggio aste Bidoo
|
|
||||||
- ? Offerte automatiche
|
|
||||||
- ? Statistiche PostgreSQL
|
|
||||||
- ? Docker support base
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Esempi Pratici
|
|
||||||
|
|
||||||
### Scenario 1: Bug Fix
|
|
||||||
|
|
||||||
**Situazione:** Corretto bug calcolo statistiche
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- Prima -->
|
|
||||||
<Version>1.1.0</Version>
|
|
||||||
|
|
||||||
<!-- Dopo -->
|
|
||||||
<Version>1.1.1</Version>
|
|
||||||
```
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## [1.1.1] - 2025-01-19
|
|
||||||
|
|
||||||
### ?? Correzioni
|
|
||||||
- Fix calcolo media offerte in Statistics.razor
|
|
||||||
```
|
|
||||||
|
|
||||||
### Scenario 2: Nuova Feature
|
|
||||||
|
|
||||||
**Situazione:** Aggiunto supporto notifiche email
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- Prima -->
|
|
||||||
<Version>1.1.1</Version>
|
|
||||||
|
|
||||||
<!-- Dopo -->
|
|
||||||
<Version>1.2.0</Version>
|
|
||||||
```
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## [1.2.0] - 2025-01-20
|
|
||||||
|
|
||||||
### ? Aggiunte
|
|
||||||
- Notifiche email per aste vinte
|
|
||||||
- Configurazione SMTP in Settings
|
|
||||||
```
|
|
||||||
|
|
||||||
### Scenario 3: Breaking Change
|
|
||||||
|
|
||||||
**Situazione:** API REST completamente ristrutturata
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- Prima -->
|
|
||||||
<Version>1.2.0</Version>
|
|
||||||
|
|
||||||
<!-- Dopo -->
|
|
||||||
<Version>2.0.0</Version>
|
|
||||||
```
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## [2.0.0] - 2025-02-01
|
|
||||||
|
|
||||||
### ?? BREAKING CHANGES
|
|
||||||
- API REST ristrutturata (endpoints modificati)
|
|
||||||
- Migrazione richiesta per client esistenti
|
|
||||||
|
|
||||||
### ?? Modifiche
|
|
||||||
- Endpoint `/api/auctions` ? `/api/v2/auctions`
|
|
||||||
- Response format JSON standardizzato
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Automazione
|
|
||||||
|
|
||||||
### GitHub Actions / Gitea Actions
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# .gitea/workflows/version-check.yml
|
|
||||||
name: Version Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main, docker ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-version:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Extract version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=$(grep -oP '<Version>\K[^<]+' AutoBidder.csproj | head -1)
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Check CHANGELOG
|
|
||||||
run: |
|
|
||||||
if ! grep -q "## \[${{ steps.version.outputs.version }}\]" CHANGELOG.md; then
|
|
||||||
echo "?? Versione ${{ steps.version.outputs.version }} non documentata in CHANGELOG.md"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create Git Tag
|
|
||||||
run: |
|
|
||||||
git tag v${{ steps.version.outputs.version }}
|
|
||||||
git push origin v${{ steps.version.outputs.version }}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PowerShell Script Locale
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# scripts/bump-version.ps1
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[ValidateSet('major','minor','patch')]
|
|
||||||
[string]$Type
|
|
||||||
)
|
|
||||||
|
|
||||||
# Leggi versione corrente
|
|
||||||
$csproj = "AutoBidder.csproj"
|
|
||||||
$content = Get-Content $csproj -Raw
|
|
||||||
$version = [regex]::Match($content, '<Version>(.*?)</Version>').Groups[1].Value
|
|
||||||
|
|
||||||
# Parse semantic version
|
|
||||||
$parts = $version -split '\.'
|
|
||||||
$major = [int]$parts[0]
|
|
||||||
$minor = [int]$parts[1]
|
|
||||||
$patch = [int]$parts[2]
|
|
||||||
|
|
||||||
# Incrementa
|
|
||||||
switch ($Type) {
|
|
||||||
'major' { $major++; $minor=0; $patch=0 }
|
|
||||||
'minor' { $minor++; $patch=0 }
|
|
||||||
'patch' { $patch++ }
|
|
||||||
}
|
|
||||||
|
|
||||||
$newVersion = "$major.$minor.$patch"
|
|
||||||
|
|
||||||
# Aggiorna .csproj
|
|
||||||
$content = $content -replace '<Version>.*?</Version>', "<Version>$newVersion</Version>"
|
|
||||||
$content = $content -replace '<AssemblyVersion>.*?</AssemblyVersion>', "<AssemblyVersion>$newVersion.0</AssemblyVersion>"
|
|
||||||
$content = $content -replace '<FileVersion>.*?</FileVersion>', "<FileVersion>$newVersion.0</FileVersion>"
|
|
||||||
$content = $content -replace '<InformationalVersion>.*?</InformationalVersion>', "<InformationalVersion>$newVersion</InformationalVersion>"
|
|
||||||
Set-Content $csproj $content
|
|
||||||
|
|
||||||
# Aggiorna Dockerfile
|
|
||||||
$dockerfile = "Dockerfile"
|
|
||||||
$dockerContent = Get-Content $dockerfile -Raw
|
|
||||||
$dockerContent = $dockerContent -replace 'org.opencontainers.image.version=".*?"', "org.opencontainers.image.version=""$newVersion"""
|
|
||||||
Set-Content $dockerfile $dockerContent
|
|
||||||
|
|
||||||
Write-Host "? Versione aggiornata: $version ? $newVersion"
|
|
||||||
Write-Host "?? Ricorda di aggiornare CHANGELOG.md!"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Uso:**
|
|
||||||
```powershell
|
|
||||||
# Incrementa PATCH (bug fix)
|
|
||||||
.\scripts\bump-version.ps1 -Type patch
|
|
||||||
|
|
||||||
# Incrementa MINOR (nuova feature)
|
|
||||||
.\scripts\bump-version.ps1 -Type minor
|
|
||||||
|
|
||||||
# Incrementa MAJOR (breaking change)
|
|
||||||
.\scripts\bump-version.ps1 -Type major
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Riferimenti
|
|
||||||
|
|
||||||
- [Semantic Versioning 2.0.0](https://semver.org/)
|
|
||||||
- [Keep a Changelog](https://keepachangelog.com/)
|
|
||||||
- [Conventional Commits](https://www.conventionalcommits.org/)
|
|
||||||
- [GitVersion](https://gitversion.net/) (tool automatico)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Checklist Release
|
|
||||||
|
|
||||||
Prima di ogni release:
|
|
||||||
|
|
||||||
- [ ] Versione incrementata in `AutoBidder.csproj`
|
|
||||||
- [ ] Versione aggiornata in `Dockerfile` labels
|
|
||||||
- [ ] Modifiche documentate in `CHANGELOG.md`
|
|
||||||
- [ ] Build locale testata
|
|
||||||
- [ ] Container Docker testato localmente
|
|
||||||
- [ ] Pubblicazione su Gitea completata
|
|
||||||
- [ ] Tag Git creato (`v1.1.0`)
|
|
||||||
- [ ] Documentazione aggiornata (se necessario)
|
|
||||||
|
|
||||||
**Dopo la release:**
|
|
||||||
|
|
||||||
- [ ] Verifica immagine su Gitea
|
|
||||||
- [ ] Test pull e deploy
|
|
||||||
- [ ] Comunicazione team (se applicabile)
|
|
||||||
- [ ] Aggiornamento deployment production
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**?? Versione corrente:** `1.1.0` - Docker/Gitea Publishing Workflow
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
# ?? SISTEMA VERSIONAMENTO IMPLEMENTATO
|
|
||||||
|
|
||||||
## ? Versione Corrente: `1.1.0`
|
|
||||||
|
|
||||||
**Data:** 2025-01-18
|
|
||||||
**Tipo:** MINOR (nuove feature + bug fix)
|
|
||||||
**Modifiche:** Docker/Gitea Publishing Workflow + HTTPS Fix
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? File Creati/Aggiornati
|
|
||||||
|
|
||||||
### Nuovi File
|
|
||||||
|
|
||||||
1. **`CHANGELOG.md`**
|
|
||||||
- Storico completo modifiche
|
|
||||||
- Formato [Keep a Changelog](https://keepachangelog.com/)
|
|
||||||
- Documentazione v1.1.0 completa
|
|
||||||
|
|
||||||
2. **`VERSIONING.md`**
|
|
||||||
- Guida sistema versionamento
|
|
||||||
- Workflow di rilascio
|
|
||||||
- Esempi pratici
|
|
||||||
- Automazione
|
|
||||||
|
|
||||||
3. **`bump-version.ps1`**
|
|
||||||
- Script PowerShell automatico
|
|
||||||
- Incrementa MAJOR/MINOR/PATCH
|
|
||||||
- Aggiorna tutti i file coinvolti
|
|
||||||
- Genera template CHANGELOG
|
|
||||||
|
|
||||||
### File Aggiornati
|
|
||||||
|
|
||||||
1. **`AutoBidder.csproj`**
|
|
||||||
```xml
|
|
||||||
<!-- v1.1.0: Docker/Gitea publishing workflow + HTTPS fix -->
|
|
||||||
<Version>1.1.0</Version>
|
|
||||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
|
||||||
<FileVersion>1.1.0.0</FileVersion>
|
|
||||||
<InformationalVersion>1.1.0</InformationalVersion>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **`Dockerfile`**
|
|
||||||
```dockerfile
|
|
||||||
LABEL org.opencontainers.image.version="1.1.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Come Usare il Sistema
|
|
||||||
|
|
||||||
### Metodo 1: Script Automatico (CONSIGLIATO)
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Bug fix (1.1.0 ? 1.1.1)
|
|
||||||
.\bump-version.ps1 -Type patch
|
|
||||||
|
|
||||||
# Nuova feature (1.1.0 ? 1.2.0)
|
|
||||||
.\bump-version.ps1 -Type minor
|
|
||||||
|
|
||||||
# Breaking change (1.1.0 ? 2.0.0)
|
|
||||||
.\bump-version.ps1 -Type major
|
|
||||||
```
|
|
||||||
|
|
||||||
**Lo script fa automaticamente:**
|
|
||||||
1. ? Incrementa versione in `AutoBidder.csproj`
|
|
||||||
2. ? Aggiorna `Dockerfile` labels
|
|
||||||
3. ? Aggiunge template in `CHANGELOG.md`
|
|
||||||
4. ? Mostra prossimi passi
|
|
||||||
|
|
||||||
### Metodo 2: Manuale
|
|
||||||
|
|
||||||
1. **Modifica `AutoBidder.csproj`:**
|
|
||||||
```xml
|
|
||||||
<Version>1.2.0</Version>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Modifica `Dockerfile`:**
|
|
||||||
```dockerfile
|
|
||||||
LABEL org.opencontainers.image.version="1.2.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Aggiorna `CHANGELOG.md`:**
|
|
||||||
```markdown
|
|
||||||
## [1.2.0] - 2025-01-19
|
|
||||||
|
|
||||||
### ? Aggiunte
|
|
||||||
- Nuova feature X
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Pubblica:**
|
|
||||||
```bash
|
|
||||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Workflow Completo di Rilascio
|
|
||||||
|
|
||||||
### Step 1: Incrementa Versione
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\bump-version.ps1 -Type minor
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Compila CHANGELOG
|
|
||||||
|
|
||||||
Apri `CHANGELOG.md` e completa il template:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## [1.2.0] - 2025-01-19
|
|
||||||
|
|
||||||
### ? Aggiunte
|
|
||||||
- Feature notifiche email per aste vinte
|
|
||||||
- Configurazione SMTP in Settings
|
|
||||||
|
|
||||||
### ?? Modifiche
|
|
||||||
- Migliorato algoritmo calcolo statistiche
|
|
||||||
|
|
||||||
### ?? Correzioni
|
|
||||||
- Fix bug crash su asta annullata
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Commit Modifiche
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add AutoBidder.csproj Dockerfile CHANGELOG.md
|
|
||||||
git commit -m "chore: bump version to v1.2.0
|
|
||||||
|
|
||||||
- Feature notifiche email
|
|
||||||
- Fix bug crash asta annullata"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Tag Git
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git tag v1.2.0
|
|
||||||
git push origin docker --tags
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 5: Pubblica Docker su Gitea
|
|
||||||
|
|
||||||
**Da Visual Studio:**
|
|
||||||
- Tasto destro ? Pubblica ? GiteaRegistry
|
|
||||||
|
|
||||||
**Da CLI:**
|
|
||||||
```bash
|
|
||||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 6: Verifica Pubblicazione
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Controlla su Gitea
|
|
||||||
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder
|
|
||||||
|
|
||||||
# Verifica tag creati
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
|
|
||||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Semantic Versioning
|
|
||||||
|
|
||||||
| Versione | Tipo | Quando Usare | Esempio |
|
|
||||||
|----------|------|--------------|---------|
|
|
||||||
| **1.0.0 ? 2.0.0** | MAJOR | Breaking changes | API cambiata, porta diversa |
|
|
||||||
| **1.0.0 ? 1.1.0** | MINOR | Nuove feature | Notifiche email, esportazione dati |
|
|
||||||
| **1.0.0 ? 1.0.1** | PATCH | Bug fix | Fix crash, correzione calcoli |
|
|
||||||
|
|
||||||
### Esempi Pratici
|
|
||||||
|
|
||||||
**Bug Fix (PATCH):**
|
|
||||||
```powershell
|
|
||||||
.\bump-version.ps1 -Type patch
|
|
||||||
# 1.1.0 ? 1.1.1
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nuova Feature (MINOR):**
|
|
||||||
```powershell
|
|
||||||
.\bump-version.ps1 -Type minor
|
|
||||||
# 1.1.1 ? 1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
**Breaking Change (MAJOR):**
|
|
||||||
```powershell
|
|
||||||
.\bump-version.ps1 -Type major
|
|
||||||
# 1.2.0 ? 2.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Tag Docker Generati
|
|
||||||
|
|
||||||
### Dopo Pubblicazione v1.1.0
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Tag su Gitea
|
|
||||||
gitea.encke-hake.ts.net/alby96/autobidder:latest ? v1.1.0
|
|
||||||
gitea.encke-hake.ts.net/alby96/autobidder:1.1.0 ? immutabile
|
|
||||||
gitea.encke-hake.ts.net/alby96/autobidder:1.0.0 ? ancora disponibile
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production Best Practice
|
|
||||||
|
|
||||||
**? NON USARE `latest` in production:**
|
|
||||||
```yaml
|
|
||||||
# ERRATO
|
|
||||||
image: gitea.../autobidder:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
**? USA versione specifica:**
|
|
||||||
```yaml
|
|
||||||
# CORRETTO
|
|
||||||
image: gitea.../autobidder:1.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
**Motivo:** `latest` cambia ad ogni release, versione specifica è immutabile.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Gestione Hotfix
|
|
||||||
|
|
||||||
### Scenario: Bug critico in production
|
|
||||||
|
|
||||||
**Production usa:** `v1.1.0`
|
|
||||||
**Development è a:** `v1.2.0-dev`
|
|
||||||
|
|
||||||
**Workflow:**
|
|
||||||
|
|
||||||
1. **Crea branch hotfix:**
|
|
||||||
```bash
|
|
||||||
git checkout -b hotfix/1.1.1 v1.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Applica fix:**
|
|
||||||
```bash
|
|
||||||
# Fix bug
|
|
||||||
.\bump-version.ps1 -Type patch # 1.1.0 ? 1.1.1
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Pubblica hotfix:**
|
|
||||||
```bash
|
|
||||||
git commit -m "fix: critical bug in auction monitoring"
|
|
||||||
git tag v1.1.1
|
|
||||||
git push origin hotfix/1.1.1 --tags
|
|
||||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Merge in main:**
|
|
||||||
```bash
|
|
||||||
git checkout docker
|
|
||||||
git merge hotfix/1.1.1
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Aggiorna development:**
|
|
||||||
```bash
|
|
||||||
# Se necessario, cherry-pick il fix in v1.2.0-dev
|
|
||||||
git cherry-pick <commit-hash>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Dashboard Versioni
|
|
||||||
|
|
||||||
### Versioni Attive
|
|
||||||
|
|
||||||
| Versione | Stato | Tag Docker | Ambiente |
|
|
||||||
|----------|-------|------------|----------|
|
|
||||||
| `1.1.0` | ? Latest | `latest`, `1.1.0` | Production |
|
|
||||||
| `1.0.0` | ?? Deprecated | `1.0.0` | Legacy |
|
|
||||||
|
|
||||||
### Roadmap
|
|
||||||
|
|
||||||
| Versione | Tipo | Piano | Data Target |
|
|
||||||
|----------|------|-------|-------------|
|
|
||||||
| `1.2.0` | MINOR | Notifiche email | Feb 2025 |
|
|
||||||
| `1.3.0` | MINOR | API REST | Mar 2025 |
|
|
||||||
| `2.0.0` | MAJOR | Refactor architettura | Q2 2025 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? Checklist Release
|
|
||||||
|
|
||||||
Prima di ogni release:
|
|
||||||
|
|
||||||
- [ ] **Versione incrementata** in `AutoBidder.csproj`
|
|
||||||
- [ ] **Versione aggiornata** in `Dockerfile`
|
|
||||||
- [ ] **CHANGELOG.md** compilato con modifiche
|
|
||||||
- [ ] **Build locale** testata
|
|
||||||
- [ ] **Container Docker** testato localmente
|
|
||||||
- [ ] **Pubblicazione Gitea** completata
|
|
||||||
- [ ] **Tag Git** creato (`v1.1.0`)
|
|
||||||
- [ ] **Documentazione** aggiornata (se necessario)
|
|
||||||
- [ ] **Migration guide** scritta (per breaking changes)
|
|
||||||
- [ ] **Communication** team/utenti (se applicabile)
|
|
||||||
|
|
||||||
Dopo la release:
|
|
||||||
|
|
||||||
- [ ] **Verifica immagine** su Gitea
|
|
||||||
- [ ] **Test pull** e deploy
|
|
||||||
- [ ] **Monitoraggio** errori prime 24h
|
|
||||||
- [ ] **Aggiornamento** deployment production
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Benefici del Sistema
|
|
||||||
|
|
||||||
### Prima (senza versioning)
|
|
||||||
- ? Versioni non tracciate
|
|
||||||
- ? Modifiche non documentate
|
|
||||||
- ? Impossibile rollback a versione specifica
|
|
||||||
- ? Difficile capire cosa è cambiato
|
|
||||||
|
|
||||||
### Dopo (con versioning)
|
|
||||||
- ? Ogni modifica tracciata con versione
|
|
||||||
- ? CHANGELOG completo e leggibile
|
|
||||||
- ? Rollback facile (`docker pull .../:1.0.0`)
|
|
||||||
- ? Deploy controllati e verificabili
|
|
||||||
- ? Automazione con script PowerShell
|
|
||||||
- ? Tag Docker immutabili per production
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? Documenti di Riferimento
|
|
||||||
|
|
||||||
| File | Scopo |
|
|
||||||
|------|-------|
|
|
||||||
| `CHANGELOG.md` | Storico modifiche per utenti |
|
|
||||||
| `VERSIONING.md` | Guida sistema per sviluppatori |
|
|
||||||
| `bump-version.ps1` | Automazione incremento versione |
|
|
||||||
| `AutoBidder.csproj` | Fonte unica della verità (versione) |
|
|
||||||
| `Dockerfile` | Metadata versione immagine |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**?? Versione attuale: `1.1.0` - Docker/Gitea Publishing Workflow**
|
|
||||||
|
|
||||||
**? Sistema di versionamento completamente implementato e operativo!**
|
|
||||||
@@ -412,8 +412,9 @@
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Rimosso effetto scale sulle righe - era fastidioso */
|
||||||
.table tbody tr:hover {
|
.table tbody tr:hover {
|
||||||
transform: scale(1.01);
|
/* transform: scale(1.01); - RIMOSSO */
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,8 +432,7 @@
|
|||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge:hover {
|
/* Rimosso effetto scale su badge hover */
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-pulse {
|
.badge-pulse {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* app-wpf.css - Modern Dark Theme */
|
/* app-wpf.css - Modern Dark Theme */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* Modern Dark Palette */
|
/* Modern Dark Palette */
|
||||||
@@ -558,6 +558,7 @@ main {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Splitter verticale tra griglia e log */
|
/* Splitter verticale tra griglia e log */
|
||||||
.splitter-vertical {
|
.splitter-vertical {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
@@ -566,22 +567,28 @@ main {
|
|||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: background 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
|
min-width: 6px;
|
||||||
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.splitter-vertical:hover {
|
.splitter-vertical:hover {
|
||||||
background: var(--primary-color);
|
background: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.splitter-vertical::after {
|
.splitter-vertical::before {
|
||||||
content: '';
|
content: '⋮';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: 2px;
|
color: var(--text-muted);
|
||||||
height: 40px;
|
font-size: 16px;
|
||||||
background: var(--text-muted);
|
opacity: 0.5;
|
||||||
border-radius: 1px;
|
}
|
||||||
|
|
||||||
|
.splitter-vertical:hover::before {
|
||||||
|
color: white;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Log globale - colonna destra */
|
/* Log globale - colonna destra */
|
||||||
@@ -598,7 +605,7 @@ main {
|
|||||||
|
|
||||||
/* Splitter orizzontale tra top e dettagli */
|
/* Splitter orizzontale tra top e dettagli */
|
||||||
.splitter-horizontal {
|
.splitter-horizontal {
|
||||||
height: 4px;
|
height: 6px;
|
||||||
background: var(--border-color);
|
background: var(--border-color);
|
||||||
cursor: row-resize;
|
cursor: row-resize;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -607,19 +614,23 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.splitter-horizontal:hover {
|
.splitter-horizontal:hover {
|
||||||
background: var(--primary-color);
|
background: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.splitter-horizontal::after {
|
.splitter-horizontal::before {
|
||||||
content: '';
|
content: '⋯';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: 40px;
|
color: var(--text-muted);
|
||||||
height: 2px;
|
font-size: 16px;
|
||||||
background: var(--text-muted);
|
opacity: 0.5;
|
||||||
border-radius: 1px;
|
}
|
||||||
|
|
||||||
|
.splitter-horizontal:hover::before {
|
||||||
|
color: white;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dettagli asta - sotto splitter orizzontale */
|
/* Dettagli asta - sotto splitter orizzontale */
|
||||||
@@ -1513,8 +1524,8 @@ main {
|
|||||||
.table-fixed .col-prezzo { width: 90px; }
|
.table-fixed .col-prezzo { width: 90px; }
|
||||||
.table-fixed .col-timer { width: 90px; }
|
.table-fixed .col-timer { width: 90px; }
|
||||||
.table-fixed .col-ultimo { width: 120px; }
|
.table-fixed .col-ultimo { width: 120px; }
|
||||||
.table-fixed .col-click { width: 70px; text-align: center; }
|
.table-fixed .col-click { width: 90px; text-align: center; padding-right: 10px; }
|
||||||
.table-fixed .col-ping { width: 80px; }
|
.table-fixed .col-ping { width: 90px; padding-left: 10px; }
|
||||||
.table-fixed .col-azioni { width: 150px; }
|
.table-fixed .col-azioni { width: 150px; }
|
||||||
|
|
||||||
.table-fixed td {
|
.table-fixed td {
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
window.Blazor.addEventListener('enhancedload', initLogScroll);
|
window.Blazor.addEventListener('enhancedload', initLogScroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Esporta funzione per forzare scroll
|
// Esporta funzione per forzare scroll
|
||||||
window.forceLogScrollToBottom = function () {
|
window.forceLogScrollToBottom = function () {
|
||||||
logBoxes.forEach(logBox => {
|
logBoxes.forEach(logBox => {
|
||||||
@@ -83,4 +84,18 @@
|
|||||||
scrollToBottom(logBox);
|
scrollToBottom(logBox);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Funzione chiamabile da Blazor per scroll specifico elemento
|
||||||
|
window.scrollToBottom = function (elementId) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (element) {
|
||||||
|
// Controlla se siamo già in fondo o quasi (entro 100px)
|
||||||
|
const isNearBottom = element.scrollHeight - element.scrollTop - element.clientHeight < 100;
|
||||||
|
|
||||||
|
// Auto-scroll solo se siamo già in fondo (non interrompe lettura manuale)
|
||||||
|
if (isNearBottom || !userScrolling.get(element)) {
|
||||||
|
element.scrollTop = element.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user