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>
|
||||
|
||||
<!-- Versioning per Docker & Gitea Registry -->
|
||||
<!-- v1.1.0: Docker/Gitea publishing workflow + HTTPS fix -->
|
||||
<Version>1.2.0</Version>
|
||||
<AssemblyVersion>1.2.0.0</AssemblyVersion>
|
||||
<FileVersion>1.2.0.0</FileVersion>
|
||||
<InformationalVersion>1.2.0</InformationalVersion>
|
||||
<!-- v1.3.0: Database management + bug fixes (duplicates, race conditions, warnings) -->
|
||||
<Version>1.3.0</Version>
|
||||
<AssemblyVersion>1.3.0.0</AssemblyVersion>
|
||||
<FileVersion>1.3.0.0</FileVersion>
|
||||
<InformationalVersion>1.3.0</InformationalVersion>
|
||||
|
||||
<!-- Metadata immagine Docker -->
|
||||
<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
|
||||
|
||||
// ???????????????????????????????????????????????????????????????
|
||||
// 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>
|
||||
|
||||
@@ -117,4 +117,62 @@ namespace AutoBidder.Models
|
||||
|
||||
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) -->
|
||||
<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
|
||||
</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
|
||||
</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
|
||||
</button>
|
||||
<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)">
|
||||
<i class="bi bi-trash"></i> Rimuovi
|
||||
</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>
|
||||
|
||||
@@ -166,7 +169,7 @@
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="log-box">
|
||||
<div class="log-box" id="globalLogContainer" @ref="globalLogRef">
|
||||
@if (globalLog.Count == 0)
|
||||
{
|
||||
<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 id="logScrollAnchor"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,21 +259,11 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 info-group">
|
||||
<label><i class="bi bi-arrow-repeat"></i> Min Reset:</label>
|
||||
<input type="number" class="form-control" @bind="selectedAuction.MinResets" @bind:after="SaveAuctions" />
|
||||
<div class="col-md-12 info-group">
|
||||
<label><i class="bi bi-hand-index-thumb"></i> Max Puntate (0 = illimitate):</label>
|
||||
<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 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>
|
||||
|
||||
<!-- Pulsante Applica Limiti Consigliati -->
|
||||
@@ -442,17 +436,27 @@
|
||||
@{
|
||||
// Crea una copia thread-safe per evitare modifiche durante l'enumerazione
|
||||
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())
|
||||
{
|
||||
// Calcola statistiche puntatori
|
||||
var bidderStats = recentBidsCopy
|
||||
.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)
|
||||
.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">
|
||||
<table class="table table-sm table-striped">
|
||||
|
||||
@@ -46,6 +46,10 @@ namespace AutoBidder.Pages
|
||||
private bool isLoadingRecommendations = false;
|
||||
private string? recommendationMessage = null;
|
||||
private bool recommendationSuccess = false;
|
||||
|
||||
// Auto-scroll log
|
||||
private ElementReference globalLogRef;
|
||||
private int lastLogCount = 0;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
@@ -91,6 +95,17 @@ namespace AutoBidder.Pages
|
||||
await JSRuntime.InvokeVoidAsync("addDeleteKeyListener",
|
||||
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
|
||||
@@ -462,6 +477,12 @@ namespace AutoBidder.Pages
|
||||
// Decodifica HTML entities
|
||||
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)
|
||||
{
|
||||
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()
|
||||
{
|
||||
if (selectedAuction == null) return;
|
||||
@@ -711,7 +768,6 @@ namespace AutoBidder.Pages
|
||||
// Stati controllati dall'utente
|
||||
if (!auction.IsActive) return "Fermata";
|
||||
if (auction.IsPaused) return "Pausa";
|
||||
if (auction.IsAttackInProgress) return "Puntando";
|
||||
return "Attiva";
|
||||
}
|
||||
|
||||
@@ -741,7 +797,6 @@ namespace AutoBidder.Pages
|
||||
// Stati controllati dall'utente
|
||||
if (!auction.IsActive) return "<i class='bi bi-stop-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>";
|
||||
}
|
||||
|
||||
@@ -849,6 +904,11 @@ namespace AutoBidder.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private string GetCurrentUsername()
|
||||
{
|
||||
return sessionUsername ?? "";
|
||||
}
|
||||
|
||||
// ?? NUOVI METODI: Visualizzazione valori prodotto
|
||||
|
||||
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>
|
||||
<input type="number" class="form-control" @bind="settings.MinimumRemainingBids" />
|
||||
</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 class="mt-3">
|
||||
@@ -178,6 +172,255 @@
|
||||
</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 -->
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-logs">
|
||||
@@ -334,7 +577,7 @@
|
||||
<h6 class="text-muted mb-2">Versione</h6>
|
||||
<div class="d-flex align-items-center">
|
||||
<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>
|
||||
@@ -418,6 +661,11 @@ private string usernameInput = "";
|
||||
private string? connectionError;
|
||||
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 System.Threading.Timer? updateTimer;
|
||||
|
||||
@@ -437,6 +685,62 @@ private System.Threading.Timer? updateTimer;
|
||||
}, 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()
|
||||
{
|
||||
if (settings.RememberAuctionStates)
|
||||
|
||||
+369
-22
@@ -2,8 +2,12 @@
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@using AutoBidder.Models
|
||||
@using AutoBidder.Services
|
||||
@using Microsoft.JSInterop
|
||||
@inject StatsService StatsService
|
||||
@inject DatabaseService DatabaseService
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject AuctionMonitor AuctionMonitor
|
||||
@inject ApplicationStateService AppState
|
||||
|
||||
<PageTitle>Statistiche - AutoBidder</PageTitle>
|
||||
|
||||
@@ -55,17 +59,58 @@
|
||||
<div class="col-lg-7">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-clock-history me-2"></i>
|
||||
Aste Terminate Recenti
|
||||
</h5>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-clock-history me-2"></i>
|
||||
Aste Terminate (@(filteredAuctions?.Count ?? 0))
|
||||
</h5>
|
||||
</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">
|
||||
@if (recentAuctions == null || !recentAuctions.Any())
|
||||
@if (filteredAuctions == null || !filteredAuctions.Any())
|
||||
{
|
||||
<div class="text-center py-5 text-muted">
|
||||
<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>
|
||||
}
|
||||
else
|
||||
@@ -74,30 +119,68 @@
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th class="text-end">Prezzo</th>
|
||||
<th class="text-end">Puntate</th>
|
||||
<th class="sortable-header" @onclick='() => SortBy("name")'>
|
||||
Nome @GetSortIndicator("name")
|
||||
</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 class="text-center">Stato</th>
|
||||
<th>Data</th>
|
||||
<th class="text-center sortable-header" @onclick='() => SortBy("won")'>
|
||||
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>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var auction in recentAuctions)
|
||||
@foreach (var auction in filteredAuctions)
|
||||
{
|
||||
<tr class="@(auction.Won ? "table-success-subtle" : "")">
|
||||
<td><small>@auction.AuctionName</small></td>
|
||||
<tr class="@(auction.Won ? "table-success-subtle" : "") auction-row"
|
||||
@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">@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 class="text-center">
|
||||
@if (auction.Won)
|
||||
{
|
||||
<span class="badge bg-success">? Vinta</span>
|
||||
<span class="badge bg-success">?</span>
|
||||
}
|
||||
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><small class="text-muted">@FormatTimestamp(auction.Timestamp)</small></td>
|
||||
@@ -198,17 +281,159 @@
|
||||
</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>
|
||||
|
||||
@code {
|
||||
private bool isLoading = true;
|
||||
private List<AuctionResultExtended>? recentAuctions;
|
||||
private List<AuctionResultExtended>? filteredAuctions;
|
||||
private List<ProductStatisticsRecord>? products;
|
||||
|
||||
[Inject] private AuctionMonitor AuctionMonitor { get; set; } = default!;
|
||||
[Inject] private ApplicationStateService AppState { get; set; } = default!;
|
||||
[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
|
||||
// Filtri e ordinamento
|
||||
private string filterName = "";
|
||||
private string filterWon = "";
|
||||
private AuctionResultExtended? selectedAuctionDetail;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -222,8 +447,9 @@ private List<ProductStatisticsRecord>? products;
|
||||
|
||||
try
|
||||
{
|
||||
// Carica aste recenti (ultime 50)
|
||||
recentAuctions = await DatabaseService.GetRecentAuctionResultsAsync(50);
|
||||
// Carica aste recenti (ultime 100 per permettere filtri)
|
||||
recentAuctions = await DatabaseService.GetRecentAuctionResultsAsync(100);
|
||||
ApplyFilters();
|
||||
|
||||
// Carica prodotti con statistiche
|
||||
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)
|
||||
{
|
||||
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
|
||||
);
|
||||
|
||||
var auctionMonitor = new AuctionMonitor();
|
||||
var bidStrategyService = new BidStrategyService();
|
||||
var auctionMonitor = new AuctionMonitor(bidStrategyService);
|
||||
htmlCacheService.OnLog += (msg) => Console.WriteLine(msg);
|
||||
|
||||
builder.Services.AddSingleton(bidStrategyService);
|
||||
builder.Services.AddSingleton(auctionMonitor);
|
||||
builder.Services.AddSingleton(htmlCacheService);
|
||||
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.Tasks;
|
||||
using AutoBidder.Models;
|
||||
using AutoBidder.Utilities;
|
||||
|
||||
namespace AutoBidder.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Servizio centrale per monitoraggio aste
|
||||
/// Sistema di timing ottimizzato: punta solo se necessario, poco prima della scadenza
|
||||
/// Integra BidStrategyService per strategie avanzate
|
||||
/// </summary>
|
||||
public class AuctionMonitor
|
||||
{
|
||||
private readonly BidooApiClient _apiClient;
|
||||
private readonly BidStrategyService _bidStrategy;
|
||||
private readonly List<AuctionInfo> _auctions = new();
|
||||
private CancellationTokenSource? _monitoringCts;
|
||||
private Task? _monitoringTask;
|
||||
@@ -29,9 +32,10 @@ namespace AutoBidder.Services
|
||||
/// </summary>
|
||||
public event Action<AuctionInfo, AuctionState, bool>? OnAuctionCompleted;
|
||||
|
||||
public AuctionMonitor()
|
||||
public AuctionMonitor(BidStrategyService? bidStrategy = null)
|
||||
{
|
||||
_apiClient = new BidooApiClient();
|
||||
_bidStrategy = bidStrategy ?? new BidStrategyService();
|
||||
|
||||
_apiClient.OnAuctionLog += (auctionId, message) =>
|
||||
{
|
||||
@@ -334,6 +338,7 @@ namespace AutoBidder.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private async Task PollAndProcessAuction(AuctionInfo auction, CancellationToken token)
|
||||
{
|
||||
try
|
||||
@@ -347,10 +352,17 @@ namespace AutoBidder.Services
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
auction.PollingLatencyMs = state.PollingLatencyMs;
|
||||
// ?? Aggiorna latenza con storico
|
||||
auction.AddLatencyMeasurement(state.PollingLatencyMs);
|
||||
|
||||
// ? AGGIORNATO: Aggiorna storia puntate mantenendo quelle vecchie
|
||||
// ?? Segna tracking dall'inizio se è la prima volta
|
||||
if (!auction.IsTrackedFromStart && auction.BidHistory.Count == 0)
|
||||
{
|
||||
auction.IsTrackedFromStart = true;
|
||||
auction.TrackingStartedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// Aggiorna storia puntate mantenendo quelle vecchie
|
||||
if (state.RecentBidsHistory != null && state.RecentBidsHistory.Count > 0)
|
||||
{
|
||||
MergeBidHistory(auction, state.RecentBidsHistory);
|
||||
@@ -365,6 +377,32 @@ namespace AutoBidder.Services
|
||||
|
||||
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.LastState = state; // Salva stato finale per statistiche
|
||||
auction.AddLog($"[ASTA TERMINATA] {statusMsg}");
|
||||
@@ -464,28 +502,137 @@ namespace AutoBidder.Services
|
||||
}
|
||||
|
||||
/// <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>
|
||||
private async Task ExecuteBidStrategy(AuctionInfo auction, AuctionState state, CancellationToken token)
|
||||
{
|
||||
var settings = SettingsManager.Load();
|
||||
|
||||
// Calcola il tempo rimanente in millisecondi
|
||||
double timerMs = state.Timer * 1000;
|
||||
|
||||
// Se siamo nella finestra di puntata (timer <= BidBeforeDeadlineMs)
|
||||
if (timerMs <= auction.BidBeforeDeadlineMs)
|
||||
// ??? CONTROLLO: Se sono già il vincitore, non fare nulla
|
||||
if (state.IsMyBid)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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] Finestra di puntata raggiunta: {timerMs:F0}ms <= {auction.BidBeforeDeadlineMs}ms");
|
||||
|
||||
// ? NUOVO: Controlla se sono già io il vincitore corrente
|
||||
if (state.IsMyBid)
|
||||
{
|
||||
auction.AddLog($"[STRATEGIA] SKIP: Sono già il vincitore corrente (ultimo bidder: {state.LastBidder})");
|
||||
return;
|
||||
}
|
||||
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);
|
||||
@@ -494,7 +641,8 @@ namespace AutoBidder.Services
|
||||
var timeSinceLastBid = DateTime.UtcNow - lastBidTime.Value;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -507,40 +655,29 @@ namespace AutoBidder.Services
|
||||
auction.IsAttackInProgress = false;
|
||||
}
|
||||
}
|
||||
// Se timer <= 0, asta già scaduta ? Non fare nulla
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Esegue la puntata con verifica opzionale dello stato dell'asta
|
||||
/// Esegue la puntata e registra metriche
|
||||
/// </summary>
|
||||
private async Task ExecuteBid(AuctionInfo auction, AuctionState state, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Se richiesto, verifica prima che l'asta sia ancora aperta
|
||||
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
|
||||
// Esegui la puntata immediatamente
|
||||
var result = await _apiClient.PlaceBidAsync(auction.AuctionId, auction.OriginalUrl);
|
||||
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
|
||||
if (result.Success)
|
||||
{
|
||||
@@ -588,8 +725,25 @@ namespace AutoBidder.Services
|
||||
|
||||
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)
|
||||
// ?? IMPORTANTE: Applica solo se BuyNowPrice è valido (> 0)
|
||||
// IMPORTANTE: Applica solo se BuyNowPrice è valido (> 0)
|
||||
// Se BuyNowPrice == 0, significa errore scraping - non bloccare le puntate
|
||||
if (auction.BuyNowPrice.HasValue &&
|
||||
auction.BuyNowPrice.Value > 0 &&
|
||||
@@ -607,8 +761,40 @@ namespace AutoBidder.Services
|
||||
}
|
||||
}
|
||||
|
||||
// ??? CONTROLLO 1: Limite minimo puntate residue
|
||||
var settings = Utilities.SettingsManager.Load();
|
||||
// ?? CONTROLLO ANTI-COLLISIONE: Rileva aste troppo "affollate"
|
||||
// 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)
|
||||
{
|
||||
var session = _apiClient.GetSession();
|
||||
@@ -749,61 +935,67 @@ namespace AutoBidder.Services
|
||||
{
|
||||
// Carica impostazioni per limite massimo
|
||||
var settings = Utilities.SettingsManager.Load();
|
||||
var maxEntries = settings?.MaxBidHistoryEntries ?? 20;
|
||||
var maxEntries = settings?.MaxBidHistoryEntries ?? 50; // Default aumentato a 50
|
||||
|
||||
// Se la lista esistente è vuota, semplicemente copia le nuove
|
||||
if (auction.RecentBids.Count == 0)
|
||||
// ?? FIX: Usa lock per thread-safety
|
||||
lock (auction.RecentBids)
|
||||
{
|
||||
auction.RecentBids = newBids.ToList();
|
||||
|
||||
// Ordina per timestamp DECRESCENTE (più recenti in cima)
|
||||
auction.RecentBids = auction.RecentBids
|
||||
.OrderByDescending(b => b.Timestamp)
|
||||
.ToList();
|
||||
|
||||
// Limita se necessario
|
||||
if (maxEntries > 0 && auction.RecentBids.Count > maxEntries)
|
||||
// Se la lista esistente è vuota, semplicemente copia le nuove
|
||||
if (auction.RecentBids.Count == 0)
|
||||
{
|
||||
auction.RecentBids = newBids.ToList();
|
||||
|
||||
// ?? FIX: Ordina per timestamp DESC, poi per prezzo DESC (per puntate stesso secondo)
|
||||
auction.RecentBids = auction.RecentBids
|
||||
.Take(maxEntries)
|
||||
.OrderByDescending(b => b.Timestamp)
|
||||
.ThenByDescending(b => b.Price)
|
||||
.ToList();
|
||||
|
||||
// Limita se necessario
|
||||
if (maxEntries > 0 && auction.RecentBids.Count > maxEntries)
|
||||
{
|
||||
auction.RecentBids = auction.RecentBids
|
||||
.Take(maxEntries)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Aggiorna statistiche bidder basandosi su RecentBids
|
||||
UpdateBidderStatsFromRecentBids(auction);
|
||||
return;
|
||||
}
|
||||
|
||||
// Aggiorna statistiche bidder basandosi su RecentBids
|
||||
UpdateBidderStatsFromRecentBids(auction);
|
||||
return;
|
||||
}
|
||||
|
||||
// Crea un HashSet delle puntate esistenti per ricerca veloce
|
||||
// Usiamo una chiave composta: timestamp + username + price per identificare univocamente una puntata
|
||||
var existingBidsKeys = new HashSet<string>(
|
||||
auction.RecentBids.Select(b => $"{b.Timestamp}_{b.Username}_{b.Price:F2}")
|
||||
);
|
||||
|
||||
// Aggiungi solo le puntate nuove (non duplicate)
|
||||
var bidsToAdd = newBids
|
||||
.Where(b => !existingBidsKeys.Contains($"{b.Timestamp}_{b.Username}_{b.Price:F2}"))
|
||||
.ToList();
|
||||
|
||||
if (bidsToAdd.Count > 0)
|
||||
{
|
||||
auction.RecentBids.AddRange(bidsToAdd);
|
||||
// Crea un HashSet delle puntate esistenti per ricerca veloce
|
||||
// Usiamo una chiave composta: timestamp + username + price per identificare univocamente una puntata
|
||||
var existingBidsKeys = new HashSet<string>(
|
||||
auction.RecentBids.Select(b => $"{b.Timestamp}_{b.Username}_{b.Price:F2}")
|
||||
);
|
||||
|
||||
// Ordina per timestamp DECRESCENTE (più recenti in cima)
|
||||
auction.RecentBids = auction.RecentBids
|
||||
.OrderByDescending(b => b.Timestamp)
|
||||
// Aggiungi solo le puntate nuove (non duplicate)
|
||||
var bidsToAdd = newBids
|
||||
.Where(b => !existingBidsKeys.Contains($"{b.Timestamp}_{b.Username}_{b.Price:F2}"))
|
||||
.ToList();
|
||||
|
||||
// Limita al numero massimo di puntate (mantieni le più recenti = prime della lista)
|
||||
if (maxEntries > 0 && auction.RecentBids.Count > maxEntries)
|
||||
if (bidsToAdd.Count > 0)
|
||||
{
|
||||
auction.RecentBids.AddRange(bidsToAdd);
|
||||
|
||||
// ?? FIX: Ordina per timestamp DESC, poi per prezzo DESC (per puntate stesso secondo)
|
||||
auction.RecentBids = auction.RecentBids
|
||||
.Take(maxEntries)
|
||||
.OrderByDescending(b => b.Timestamp)
|
||||
.ThenByDescending(b => b.Price)
|
||||
.ToList();
|
||||
|
||||
// Limita al numero massimo di puntate (mantieni le più recenti = prime della lista)
|
||||
if (maxEntries > 0 && auction.RecentBids.Count > maxEntries)
|
||||
{
|
||||
auction.RecentBids = auction.RecentBids
|
||||
.Take(maxEntries)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Aggiorna statistiche bidder basandosi su RecentBids
|
||||
UpdateBidderStatsFromRecentBids(auction);
|
||||
}
|
||||
|
||||
// Aggiorna statistiche bidder basandosi su RecentBids
|
||||
UpdateBidderStatsFromRecentBids(auction);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -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);
|
||||
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
|
||||
|
||||
@@ -596,6 +596,142 @@ namespace AutoBidder.Services
|
||||
await using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
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>
|
||||
/// Aggiorna o inserisce statistiche aggregate per un prodotto
|
||||
/// </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
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/* Rimosso effetto scale sulle righe - era fastidioso */
|
||||
.table tbody tr:hover {
|
||||
transform: scale(1.01);
|
||||
/* transform: scale(1.01); - RIMOSSO */
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -431,8 +432,7 @@
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.badge:hover {
|
||||
transform: scale(1.1);
|
||||
/* Rimosso effetto scale su badge hover */
|
||||
}
|
||||
|
||||
.badge-pulse {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* app-wpf.css - Modern Dark Theme */
|
||||
/* app-wpf.css - Modern Dark Theme */
|
||||
|
||||
:root {
|
||||
/* Modern Dark Palette */
|
||||
@@ -558,6 +558,7 @@ main {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
/* Splitter verticale tra griglia e log */
|
||||
.splitter-vertical {
|
||||
grid-column: 2;
|
||||
@@ -566,22 +567,28 @@ main {
|
||||
cursor: col-resize;
|
||||
position: relative;
|
||||
transition: background 0.2s ease;
|
||||
min-width: 6px;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.splitter-vertical:hover {
|
||||
background: var(--primary-color);
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.splitter-vertical::after {
|
||||
content: '';
|
||||
.splitter-vertical::before {
|
||||
content: '⋮';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 2px;
|
||||
height: 40px;
|
||||
background: var(--text-muted);
|
||||
border-radius: 1px;
|
||||
color: var(--text-muted);
|
||||
font-size: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.splitter-vertical:hover::before {
|
||||
color: white;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Log globale - colonna destra */
|
||||
@@ -598,7 +605,7 @@ main {
|
||||
|
||||
/* Splitter orizzontale tra top e dettagli */
|
||||
.splitter-horizontal {
|
||||
height: 4px;
|
||||
height: 6px;
|
||||
background: var(--border-color);
|
||||
cursor: row-resize;
|
||||
position: relative;
|
||||
@@ -607,19 +614,23 @@ main {
|
||||
}
|
||||
|
||||
.splitter-horizontal:hover {
|
||||
background: var(--primary-color);
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.splitter-horizontal::after {
|
||||
content: '';
|
||||
.splitter-horizontal::before {
|
||||
content: '⋯';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
background: var(--text-muted);
|
||||
border-radius: 1px;
|
||||
color: var(--text-muted);
|
||||
font-size: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.splitter-horizontal:hover::before {
|
||||
color: white;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Dettagli asta - sotto splitter orizzontale */
|
||||
@@ -1513,8 +1524,8 @@ main {
|
||||
.table-fixed .col-prezzo { width: 90px; }
|
||||
.table-fixed .col-timer { width: 90px; }
|
||||
.table-fixed .col-ultimo { width: 120px; }
|
||||
.table-fixed .col-click { width: 70px; text-align: center; }
|
||||
.table-fixed .col-ping { width: 80px; }
|
||||
.table-fixed .col-click { width: 90px; text-align: center; padding-right: 10px; }
|
||||
.table-fixed .col-ping { width: 90px; padding-left: 10px; }
|
||||
.table-fixed .col-azioni { width: 150px; }
|
||||
|
||||
.table-fixed td {
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
window.Blazor.addEventListener('enhancedload', initLogScroll);
|
||||
}
|
||||
|
||||
|
||||
// Esporta funzione per forzare scroll
|
||||
window.forceLogScrollToBottom = function () {
|
||||
logBoxes.forEach(logBox => {
|
||||
@@ -83,4 +84,18 @@
|
||||
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