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:
2026-01-28 11:37:40 +01:00
parent 77eb9943d0
commit ae861e78d2
42 changed files with 2382 additions and 8805 deletions
+5 -5
View File
@@ -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>
-767
View File
@@ -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)
-296
View File
@@ -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! ??
-104
View File
@@ -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! ??**
-505
View File
@@ -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`
-272
View File
@@ -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! ??
-309
View File
@@ -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!**
-402
View File
@@ -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!**
-386
View File
@@ -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!
```
-408
View File
@@ -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!**
-241
View File
@@ -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!**
-304
View File
@@ -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!**
+169
View File
@@ -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>
+58
View File
@@ -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;
}
}
-250
View File
@@ -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! ??
-274
View File
@@ -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.
-214
View File
@@ -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 &quot;$(DockerfilePath)&quot; &quot;$(DockerfileContext)&quot;" />
<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
View File
@@ -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">
+62 -2
View File
@@ -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("&plus;", "+")
.Replace("&amp;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)
+311 -7
View File
@@ -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 &lt; 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
View File
@@ -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
View File
@@ -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()));
-211
View File
@@ -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!**
-280
View File
@@ -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! ??
-289
View File
@@ -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!** ??
-376
View File
@@ -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
-427
View File
@@ -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!**
-261
View File
@@ -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!**
-411
View File
@@ -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.
+275 -83
View File
@@ -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)
+501
View File
@@ -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; }
}
}
+7 -1
View File
@@ -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("&plus;", "+")
.Replace("&amp;plus;", "+")
.Replace(" + ", " & ");
auction.Name = name;
}
// Estrai prezzo compralo subito
+307
View File
@@ -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>
-410
View File
@@ -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!**
+244
View File
@@ -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
-183
View File
@@ -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!
-305
View File
@@ -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
-340
View File
@@ -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!**
+3 -3
View File
@@ -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 {
+29 -18
View File
@@ -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 {
+15
View File
@@ -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;
}
}
};
})();