Compare commits

...

7 Commits

Author SHA1 Message Date
690f7e636a Ottimizzazione RAM, UI e sistema di timing aste
- Ridotto consumo RAM: limiti log, pulizia e compattazione dati aste, timer periodico di cleanup
- UI più fluida: cache locale aste, throttling aggiornamenti, refresh log solo se necessario
- Nuovo sistema Ticker Loop: timing configurabile, strategie solo vicino alla scadenza, feedback puntate tardive
- Migliorato layout e splitter, log visivo, gestione cache HTML
- Aggiornata UI impostazioni e fix vari per performance e thread-safety
2026-02-07 19:28:30 +01:00
5b95f18889 Restyling monitor aste: toolbar compatta, split panel, UX
- Nuova toolbar compatta con azioni rapide e indicatori stato aste
- Layout a pannelli ridimensionabili con splitter drag&drop
- Tabella aste compatta, ping colorato, azioni XS
- Pulsanti per rimozione aste per stato (attive, vinte, ecc.)
- Dettagli asta sempre visibili in pannello inferiore
- Statistiche prodotti: filtro, ordinamento, editing limiti default
- Limiti default prodotto salvati in DB, applicabili a tutte le aste
- Migliorata sidebar utente con info sessione sempre visibili
- Log motivi blocco puntata sempre visibili, suggerimenti timing
- Miglioramenti filtri, UX responsive, fix minori e feedback visivi
2026-02-06 15:35:53 +01:00
45dd205270 Miglioramento commenti e semplificazione logica puntata
Correzione della codifica dei caratteri speciali nei commenti e nei log, aggiunta dei namespace mancanti, semplificazione della condizione per la puntata automatica e aggiornamento dei simboli di valuta. Refactoring generale dei commenti per maggiore chiarezza e manutenzione, senza modifiche alla logica principale.
2026-02-05 09:36:40 +01:00
0764b0b625 Semplifica timing puntata, logging e controllo convenienza
- Timing di puntata ora gestito solo da offset fisso configurabile, rimosse strategie di compensazione latenza/jitter/offset dinamico
- Aggiunto controllo convenienza: blocca puntate se il costo supera il "Compra Subito" oltre una soglia configurabile
- Logging granulare: nuove opzioni per log selettivo (puntate, strategie, valore, competizione, timing, errori, stato, profiling avversari)
- Persistenza stato browser aste (categoria, ricerca) tramite ApplicationStateService
- Fix conteggio puntate per bidder, rimosso rilevamento "Last Second Sniper", aggiunta strategia "Price Momentum"
- Refactoring e pulizia: rimozione codice obsoleto, migliorata documentazione e thread-safety
2026-02-05 09:28:58 +01:00
8befcb8abf Rework UI, log e strategie; fix selezione aste
- Interfaccia impostazioni più compatta e responsive, rimosse animazioni popup su hover, evidenziazione con colore
- Ottimizzazione visualizzazione puntate e statistiche, evidenza puntate proprie
- Rework sistema di log: eliminazione duplicati e info inutili, maggiore leggibilità
- Aggiunti nuovi stati e motivazioni per cui il bot non punta (fuori range, strategia, ecc)
- Fix critico: selezione aste ora sempre aggiornata e salvata correttamente
- Migliorata logica aggiunta puntate mancanti, niente duplicati
- Rimossa logica errata "Entry Point": limiti utente ora rigidi, usato solo per suggerimenti
- Aggiornata documentazione e guide per riflettere le nuove funzionalità
2026-02-03 10:50:51 +01:00
89aed8a458 Migliorie UI, log aste, strategie e statistiche puntatori
- Ordinamento colonne griglia aste e indicatori visivi
- Nuovo pulsante per rimozione rapida aste terminate
- Log aste con deduplicazione e contatore
- Statistiche puntatori cumulative e più affidabili
- Cronologia puntate senza duplicati consecutivi
- Strategie di puntata semplificate: entry point, anti-bot, user exhaustion
- UI più compatta, hover moderni, evidenziazione puntate utente
- Correzioni internazionalizzazione e pulizia codice
2026-02-03 00:00:33 +01:00
ae861e78d2 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.
2026-01-28 11:37:40 +01:00
55 changed files with 6252 additions and 10750 deletions

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>

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)

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! ??

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! ??**

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`

View File

@@ -1,76 +0,0 @@
# Sezione Configurazione Database - Impostazioni
## ?? Nota Implementazione
La configurazione del database PostgreSQL è già completamente funzionante tramite:
1. **appsettings.json** - Connection strings e configurazione
2. **AppSettings** (Utilities/SettingsManager.cs) - Proprietà salvate:
- `UsePostgreSQL`
- `PostgresConnectionString`
- `AutoCreateDatabaseSchema`
- `FallbackToSQLite`
3. **Program.cs** - Inizializzazione automatica database
## ?? UI Settings (Opzionale)
Se si desidera aggiungere una sezione nella pagina `Settings.razor` per configurare PostgreSQL tramite UI,
le proprietà sono già disponibili nel modello `AppSettings`.
### Esempio Codice UI
```razor
<!-- CONFIGURAZIONE DATABASE -->
<div class="card mb-4">
<div class="card-header bg-secondary text-white">
<h5><i class="bi bi-database-fill"></i> Configurazione Database</h5>
</div>
<div class="card-body">
<div class="form-check form-switch mb-3">
<input type="checkbox" class="form-check-input" id="usePostgres" @bind="settings.UsePostgreSQL" />
<label class="form-check-label" for="usePostgres">
Usa PostgreSQL per Statistiche Avanzate
</label>
</div>
@if (settings.UsePostgreSQL)
{
<div class="mb-3">
<label class="form-label">PostgreSQL Connection String:</label>
<input type="text" class="form-control" @bind="settings.PostgresConnectionString" />
</div>
<div class="form-check mb-2">
<input type="checkbox" class="form-check-input" id="autoCreate" @bind="settings.AutoCreateDatabaseSchema" />
<label class="form-check-label" for="autoCreate">
Auto-crea schema se mancante
</label>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="fallback" @bind="settings.FallbackToSQLite" />
<label class="form-check-label" for="fallback">
Fallback a SQLite se PostgreSQL non disponibile
</label>
</div>
}
<button class="btn btn-secondary" @onclick="SaveSettings">
Salva Configurazione Database
</button>
</div>
</div>
```
## ? Stato Attuale
**Il database PostgreSQL funziona perfettamente configurandolo tramite:**
- `appsettings.json` (Development)
- Variabili ambiente `.env` (Production/Docker)
**Non è necessaria una UI se la configurazione rimane statica.**
---
Per maggiori dettagli vedi: `Documentation/POSTGRESQL_SETUP.md`

View File

@@ -1,339 +0,0 @@
# ?? IMPLEMENTAZIONE COMPLETA - PostgreSQL + UI Impostazioni
## ? **STATO FINALE: 100% COMPLETATO**
Tutte le funzionalità PostgreSQL sono state implementate e integrate con UI completa nella pagina Impostazioni.
---
## ?? **COMPONENTI IMPLEMENTATI**
### 1. **Backend PostgreSQL** ?
| Componente | File | Status |
|------------|------|--------|
| DbContext | `Data/PostgresStatsContext.cs` | ? Completo |
| Modelli | `Models/PostgresModels.cs` | ? 5 entità |
| Service | `Services/StatsService.cs` | ? Dual-DB |
| Configuration | `Program.cs` | ? Auto-init |
| Settings Model | `Utilities/SettingsManager.cs` | ? Proprietà DB |
### 2. **Frontend UI** ?
| Componente | File | Descrizione |
|------------|------|-------------|
| Settings Page | `Pages/Settings.razor` | ? Sezione DB completa |
| Connection Test | Settings code-behind | ? Test PostgreSQL |
| Documentation | `Documentation/` | ? 2 guide |
---
## ?? **UI SEZIONE DATABASE**
### **Layout Completo**
```
??????????????????????????????????????????????
? ?? Configurazione Database ?
??????????????????????????????????????????????
? ?? Database Dual-Mode: ?
? PostgreSQL per statistiche avanzate ?
? + SQLite come fallback locale ?
??????????????????????????????????????????????
? ?? Usa PostgreSQL per Statistiche Avanzate?
? ?
? ?? PostgreSQL Connection String: ?
? [Host=localhost;Port=5432;...] ?
? ?
? ?? Auto-crea schema database se mancante ?
? ?? Fallback automatico a SQLite ?
? ?
? ?? Configurazione Docker: [info box] ?
? ?
? [?? Test Connessione PostgreSQL] ?
? ? Connessione riuscita! PostgreSQL 16 ?
? ?
? [?? Salva Configurazione Database] ?
??????????????????????????????????????????????
```
---
## ?? **FUNZIONALITÀ UI**
### **1. Toggle PostgreSQL**
```razor
<input type="checkbox" @bind="settings.UsePostgreSQL" />
```
- Abilita/disabilita PostgreSQL
- Mostra/nasconde opzioni avanzate
### **2. Connection String Editor**
```razor
<input type="text" @bind="settings.PostgresConnectionString"
class="font-monospace" />
```
- Input monospaziato per leggibilità
- Placeholder con esempio formato
### **3. Auto-Create Schema**
```razor
<input type="checkbox" @bind="settings.AutoCreateDatabaseSchema" />
```
- Crea automaticamente tabelle al primo avvio
- Default: `true` (consigliato)
### **4. Fallback SQLite**
```razor
<input type="checkbox" @bind="settings.FallbackToSQLite" />
```
- Usa SQLite se PostgreSQL non disponibile
- Default: `true` (garantisce continuità)
### **5. Test Connessione**
```csharp
private async Task TestDatabaseConnection()
{
await using var conn = new Npgsql.NpgsqlConnection(connString);
await conn.OpenAsync();
var cmd = new Npgsql.NpgsqlCommand("SELECT version()", conn);
var version = await cmd.ExecuteScalarAsync();
dbTestResult = $"Connessione riuscita! PostgreSQL {version}";
dbTestSuccess = true;
}
```
**Output:**
- ? Verde: Connessione riuscita + versione
- ? Rosso: Errore con messaggio dettagliato
---
## ?? **PERSISTENZA CONFIGURAZIONE**
### **File JSON Locale**
```json
// %LOCALAPPDATA%/AutoBidder/settings.json
{
"UsePostgreSQL": true,
"PostgresConnectionString": "Host=localhost;Port=5432;...",
"AutoCreateDatabaseSchema": true,
"FallbackToSQLite": true
}
```
### **Caricamento Automatico**
```csharp
protected override void OnInitialized()
{
settings = AutoBidder.Utilities.SettingsManager.Load();
}
```
### **Salvataggio Click**
```csharp
private void SaveSettings()
{
AutoBidder.Utilities.SettingsManager.Save(settings);
await JSRuntime.InvokeVoidAsync("alert", "? Salvato!");
}
```
---
## ?? **INTEGRAZIONE PROGRAM.CS**
```csharp
// Legge impostazioni da AppSettings
var usePostgres = builder.Configuration.GetValue<bool>("Database:UsePostgres");
// Applica configurazione da settings.json
var settings = AutoBidder.Utilities.SettingsManager.Load();
if (settings.UsePostgreSQL)
{
builder.Services.AddDbContext<PostgresStatsContext>(options =>
{
options.UseNpgsql(settings.PostgresConnectionString);
});
}
```
---
## ?? **DOCUMENTAZIONE CREATA**
### **1. Setup Guide**
**File:** `Documentation/POSTGRESQL_SETUP.md`
**Contenuto:**
- Quick Start (Development + Production)
- Schema tabelle completo
- Configurazione Docker Compose
- Query SQL utili
- Troubleshooting
- Backup/Restore
- Performance tuning
### **2. UI Template**
**File:** `Documentation/DATABASE_SETTINGS_UI.md`
**Contenuto:**
- Template Razor per UI
- Esempio code-behind
- Best practices
- Stato implementazione
---
## ?? **DEPLOYMENT**
### **Development**
```sh
# 1. Avvia PostgreSQL locale
docker run -d --name autobidder-postgres \
-e POSTGRES_DB=autobidder_stats \
-e POSTGRES_USER=autobidder \
-e POSTGRES_PASSWORD=autobidder_password \
-p 5432:5432 postgres:16-alpine
# 2. Configura in UI
http://localhost:5000/settings
? Sezione "Configurazione Database"
? Usa PostgreSQL: ?
? Connection String: Host=localhost;Port=5432;...
? Test Connessione ? ? Successo
? Salva Configurazione Database
# 3. Riavvia applicazione
dotnet run
```
### **Production (Docker Compose)**
```sh
# 1. Configura .env
POSTGRES_PASSWORD=your_secure_password_here
# 2. Deploy
docker-compose up -d
# 3. Verifica logs
docker-compose logs -f autobidder
# [PostgreSQL] Connection successful
# [PostgreSQL] Schema created successfully
# [PostgreSQL] Statistics features ENABLED
```
---
## ? **FEATURES COMPLETATE**
### **Backend**
- ? 5 tabelle PostgreSQL auto-create
- ? Migrazione schema automatica
- ? Fallback graceful a SQLite
- ? Dual-database architecture
- ? StatsService con PostgreSQL + SQLite
- ? Connection pooling
- ? Retry logic (3 tentativi)
- ? Transaction support
### **Frontend**
- ? UI Sezione Database in Settings
- ? Toggle enable/disable PostgreSQL
- ? Connection string editor
- ? Auto-create schema checkbox
- ? Fallback SQLite checkbox
- ? Test connessione con feedback visivo
- ? Info box configurazione Docker
- ? Salvataggio persistente settings
### **Documentazione**
- ? Setup guide completa
- ? Template UI opzionale
- ? Schema tabelle documentato
- ? Query esempi SQL
- ? Troubleshooting guide
- ? Docker Compose configurato
---
## ?? **STATISTICHE PROGETTO**
```
? Build Successful
? 0 Errors
? 0 Warnings
?? Files Created: 4
- Data/PostgresStatsContext.cs
- Models/PostgresModels.cs
- Documentation/POSTGRESQL_SETUP.md
- Documentation/DATABASE_SETTINGS_UI.md
?? Files Modified: 6
- AutoBidder.csproj (+ Npgsql package)
- Services/StatsService.cs
- Utilities/SettingsManager.cs (+ DB properties)
- Program.cs (+ PostgreSQL init)
- appsettings.json (+ connection strings)
- Pages/Settings.razor (+ UI section)
?? Total Lines Added: ~2,000
?? Total Lines Modified: ~300
?? Features: 100% Complete
?? Tests: Build ?
?? Documentation: 100% Complete
```
---
## ?? **TESTING CHECKLIST**
### **UI Testing**
- [ ] Aprire pagina Settings
- [ ] Verificare presenza sezione "Configurazione Database"
- [ ] Toggle PostgreSQL on/off
- [ ] Modificare connection string
- [ ] Click "Test Connessione" senza PostgreSQL ? ? Errore
- [ ] Avviare PostgreSQL Docker
- [ ] Click "Test Connessione" ? ? Successo
- [ ] Click "Salva Configurazione"
- [ ] Riavviare app e verificare settings persistiti
### **Backend Testing**
- [ ] PostgreSQL disponibile ? Tabelle auto-create
- [ ] PostgreSQL non disponibile ? Fallback SQLite
- [ ] Registrazione asta conclusa ? Dati in DB
- [ ] Query statistiche ? Risultati corretti
- [ ] Connection retry ? 3 tentativi
---
## ?? **CONCLUSIONE**
**Sistema PostgreSQL completamente integrato con:**
? **Backend completo** - 5 tabelle, dual-DB, auto-init
? **Frontend UI** - Sezione Settings con tutte le opzioni
? **Test connessione** - Feedback real-time
? **Documentazione** - 2 guide complete
? **Docker ready** - docker-compose configurato
? **Production ready** - Fallback graceful implementato
---
**Il progetto AutoBidder ora dispone di un sistema completo per statistiche avanzate con PostgreSQL, configurabile tramite UI intuitiva e con documentazione completa!** ????
---
## ?? **RIFERIMENTI**
- Setup Guide: `Documentation/POSTGRESQL_SETUP.md`
- UI Template: `Documentation/DATABASE_SETTINGS_UI.md`
- Settings Model: `Utilities/SettingsManager.cs`
- DB Context: `Data/PostgresStatsContext.cs`
- Stats Service: `Services/StatsService.cs`
- Settings UI: `Pages/Settings.razor`

View File

@@ -0,0 +1,26 @@
______________________________________________________________________________________________________________
FUNZIONALITA
Cambiare la pagina delle statistiche in modo da aggiungere una sezione in più, oltre alle statistiche memorizzate in un automatico, in cui posso associare un range di prezzo e di puntate per ogni articolo, identificato tramite il suo nome
Aggiungere una scansione periodica e automatica delle aste terminate in modo da aggiornare automaticamente il mio elenco degli articoli delle aste terminate per aggiornare prezzo e numero di puntate usate in automatico. Molto importante: salvare anche l'ora di chiusura dell'asta
Aggiungere una funzionalità di aggiunta automatica delle aste al monitor appena compaiono nell'elenco delle aste disponibile cercando tramite sezione e nome articolo
Aggiungi una indicazione visiva nella colonna dello stato che indica quando un'asta pur essendo nello stato attiva il bot non punta perché fuori range oppure per altri motivi
Fare una tasto nelle statistiche che applichi massivamente i limiti a tutti gli articoli attualmente monitorati che hanno delle informazioni salvate nel database delle aste terminate
_______________________________________________________________________________________________________________
REWORK
Esegui un rework generico del sistema di log della singola asta e del log globale. Ci sono troppe righe inutili come tante righe simili duplicate nel log della singola asta e informazioni inutili nel log globale come per esempio l'indicazione del focus che si sposta su una certa riga. Valuta i cambiamenti e le ottimizzazioni da fare e applica le modifiche.
Esegui un rework della grafica in modo da eliminare le animazioni popup che danno fastidio all'usabilità del programma. In particolare intendo che quando il mouse passa su un pulsante o una griglia questa aumenta leggermente di dimensione per evidenziarsi ma questo non mi piace. Elimina questa cosa e sostituiscila piuttosto con una illuminazione o colorazione più chiara o scura per evidenziare il fatto che sto per selezionare quel particolare pulsante
_______________________________________________________________________________________________________________
CORREZIONI
Aggiungi più stati per indicare la strategia o il fatto che non sta puntando e per quale motivo.
In particolare oltre agli stati già presenti indicare anche il motivo per cui non sta puntando come per esempio "fuori range di prezzo", "fuori range di puntate", "asta terminata", "strategia non permette puntata", ecc

View File

@@ -1,363 +0,0 @@
# PostgreSQL Setup - AutoBidder Statistics
## ?? Overview
AutoBidder utilizza PostgreSQL per statistiche avanzate e analisi strategiche delle aste concluse. Il sistema supporta **dual-database**:
- **PostgreSQL**: Statistiche persistenti e analisi avanzate
- **SQLite**: Fallback locale se PostgreSQL non disponibile
---
## ?? Quick Start
### Development (Locale)
```bash
# 1. Avvia PostgreSQL con Docker
docker run -d \
--name autobidder-postgres \
-e POSTGRES_DB=autobidder_stats \
-e POSTGRES_USER=autobidder \
-e POSTGRES_PASSWORD=autobidder_password \
-p 5432:5432 \
postgres:16-alpine
# 2. Avvia AutoBidder
dotnet run
# 3. Verifica logs
# Dovresti vedere:
# [PostgreSQL] Connection successful
# [PostgreSQL] Schema created successfully
# [PostgreSQL] Statistics features ENABLED
```
### Production (Docker Compose)
```bash
# 1. Configura variabili ambiente
cp .env.example .env
nano .env # Modifica POSTGRES_PASSWORD
# 2. Avvia stack completo
docker-compose up -d
# 3. Verifica stato
docker-compose ps
docker-compose logs -f autobidder
docker-compose logs -f postgres
```
---
## ?? Schema Database
### Tabelle Create Automaticamente
#### `completed_auctions`
Aste concluse con dettagli completi per analisi strategiche.
| Colonna | Tipo | Descrizione |
|---------|------|-------------|
| id | SERIAL | Primary key |
| auction_id | VARCHAR(100) | ID univoco asta (indexed) |
| product_name | VARCHAR(500) | Nome prodotto (indexed) |
| final_price | DECIMAL(10,2) | Prezzo finale |
| buy_now_price | DECIMAL(10,2) | Prezzo "Compra Subito" |
| total_bids | INTEGER | Puntate totali asta |
| my_bids_count | INTEGER | Mie puntate |
| won | BOOLEAN | Asta vinta? (indexed) |
| winner_username | VARCHAR(100) | Username vincitore |
| average_latency | DECIMAL(10,2) | Latency media (ms) |
| savings | DECIMAL(10,2) | Risparmio effettivo |
| completed_at | TIMESTAMP | Data/ora completamento (indexed) |
#### `product_statistics`
Statistiche aggregate per prodotto.
| Colonna | Tipo | Descrizione |
|---------|------|-------------|
| id | SERIAL | Primary key |
| product_key | VARCHAR(200) | Chiave univoca prodotto (unique) |
| product_name | VARCHAR(500) | Nome prodotto |
| average_winning_bids | DECIMAL(10,2) | Media puntate vincenti |
| recommended_max_bids | INTEGER | **Suggerimento strategico** |
| recommended_max_price | DECIMAL(10,2) | **Suggerimento strategico** |
| competition_level | VARCHAR(20) | Low/Medium/High |
| last_updated | TIMESTAMP | Ultimo aggiornamento |
#### `bidder_performances`
Performance puntatori concorrenti.
| Colonna | Tipo | Descrizione |
|---------|------|-------------|
| id | SERIAL | Primary key |
| username | VARCHAR(100) | Username puntatore (unique) |
| total_auctions | INTEGER | Aste totali |
| auctions_won | INTEGER | Aste vinte |
| win_rate | DECIMAL(5,2) | Percentuale vittorie (indexed) |
| average_bids_per_auction | DECIMAL(10,2) | Media puntate/asta |
| is_aggressive | BOOLEAN | Puntatore aggressivo? |
#### `daily_metrics`
Metriche giornaliere aggregate.
| Colonna | Tipo | Descrizione |
|---------|------|-------------|
| id | SERIAL | Primary key |
| date | DATE | Data (unique) |
| total_bids_used | INTEGER | Puntate usate |
| money_spent | DECIMAL(10,2) | Spesa totale |
| win_rate | DECIMAL(5,2) | Win rate giornaliero |
| roi | DECIMAL(10,2) | **ROI %** |
#### `strategic_insights`
Raccomandazioni strategiche generate automaticamente.
| Colonna | Tipo | Descrizione |
|---------|------|-------------|
| id | SERIAL | Primary key |
| insight_type | VARCHAR(50) | Tipo insight (indexed) |
| product_key | VARCHAR(200) | Prodotto riferimento |
| recommended_action | TEXT | **Azione consigliata** |
| confidence_level | DECIMAL(5,2) | Livello confidenza (0-100) |
| is_active | BOOLEAN | Insight attivo? |
---
## ?? Configurazione
### `appsettings.json`
```json
{
"ConnectionStrings": {
"PostgresStats": "Host=localhost;Port=5432;Database=autobidder_stats;Username=autobidder;Password=autobidder_password",
"PostgresStatsProduction": "Host=postgres;Port=5432;Database=autobidder_stats;Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}"
},
"Database": {
"UsePostgres": true,
"AutoCreateSchema": true,
"FallbackToSQLite": true
}
}
```
### `.env` (Production)
```env
# PostgreSQL
POSTGRES_USER=autobidder
POSTGRES_PASSWORD=your_secure_password_here
POSTGRES_DB=autobidder_stats
# Database config
DATABASE_USE_POSTGRES=true
DATABASE_AUTO_CREATE_SCHEMA=true
DATABASE_FALLBACK_TO_SQLITE=true
```
---
## ?? Utilizzo API
### Registra Asta Conclusa
```csharp
// Chiamato automaticamente da AuctionMonitor
await statsService.RecordAuctionCompletedAsync(auction, won: true);
```
### Ottieni Raccomandazioni Strategiche
```csharp
// Raccomandazioni per prodotto specifico
var productKey = GenerateProductKey("iPhone 15 Pro");
var insights = await statsService.GetStrategicInsightsAsync(productKey);
foreach (var insight in insights)
{
Console.WriteLine($"{insight.InsightType}: {insight.RecommendedAction}");
Console.WriteLine($"Confidence: {insight.ConfidenceLevel}%");
}
```
### Analisi Competitori
```csharp
// Top 10 puntatori più vincenti
var competitors = await statsService.GetTopCompetitorsAsync(10);
foreach (var competitor in competitors)
{
Console.WriteLine($"{competitor.Username}: {competitor.WinRate}% win rate");
if (competitor.IsAggressive)
{
Console.WriteLine(" ?? AGGRESSIVE BIDDER - Avoid competition");
}
}
```
### Statistiche Prodotto
```csharp
// Ottieni statistiche per strategia bidding
var productKey = GenerateProductKey("PlayStation 5");
var stat = await postgresDb.ProductStatistics
.FirstOrDefaultAsync(p => p.ProductKey == productKey);
if (stat != null)
{
Console.WriteLine($"Recommended max bids: {stat.RecommendedMaxBids}");
Console.WriteLine($"Recommended max price: €{stat.RecommendedMaxPrice}");
Console.WriteLine($"Competition level: {stat.CompetitionLevel}");
}
```
---
## ?? Troubleshooting
### PostgreSQL non si connette
```
[PostgreSQL] Cannot connect to database
[PostgreSQL] Statistics features will use SQLite fallback
```
**Soluzione:**
1. Verifica che PostgreSQL sia in esecuzione: `docker ps | grep postgres`
2. Controlla connection string in `appsettings.json`
3. Verifica credenziali in `.env`
4. Check logs PostgreSQL: `docker logs autobidder-postgres`
### Schema non creato
```
[PostgreSQL] Schema validation failed
[PostgreSQL] Statistics features DISABLED (missing tables)
```
**Soluzione:**
1. Abilita auto-creazione in `appsettings.json`: `"AutoCreateSchema": true`
2. Riavvia applicazione: `docker-compose restart autobidder`
3. Verifica permessi utente PostgreSQL
4. Check logs dettagliati: `docker-compose logs -f autobidder`
### Fallback a SQLite
Se PostgreSQL non è disponibile, AutoBidder usa automaticamente SQLite locale:
- ? Nessun downtime
- ? Statistiche base funzionanti
- ?? Insight strategici disabilitati
---
## ?? Backup PostgreSQL
### Manuale
```bash
# Backup database
docker exec autobidder-postgres pg_dump -U autobidder autobidder_stats > backup.sql
# Restore
docker exec -i autobidder-postgres psql -U autobidder autobidder_stats < backup.sql
```
### Automatico (con Docker Compose)
```bash
# Backup in ./postgres-backups/
docker-compose exec postgres pg_dump -U autobidder autobidder_stats \
> ./postgres-backups/backup_$(date +%Y%m%d_%H%M%S).sql
```
---
## ?? Monitoraggio
### Connessione Database
```bash
# Entra in PostgreSQL shell
docker exec -it autobidder-postgres psql -U autobidder -d autobidder_stats
# Query utili
SELECT COUNT(*) FROM completed_auctions;
SELECT COUNT(*) FROM product_statistics;
SELECT * FROM daily_metrics ORDER BY date DESC LIMIT 7;
```
### Statistiche Utilizzo
```sql
-- Aste concluse per giorno (ultimi 30 giorni)
SELECT
DATE(completed_at) as date,
COUNT(*) as total_auctions,
SUM(CASE WHEN won THEN 1 ELSE 0 END) as won,
ROUND(AVG(my_bids_count), 2) as avg_bids
FROM completed_auctions
WHERE completed_at >= NOW() - INTERVAL '30 days'
GROUP BY DATE(completed_at)
ORDER BY date DESC;
-- Top 10 prodotti più competitivi
SELECT
product_name,
total_auctions,
average_winning_bids,
competition_level
FROM product_statistics
ORDER BY average_winning_bids DESC
LIMIT 10;
```
---
## ?? Performance
### Indici Creati Automaticamente
- `idx_auction_id` su `completed_auctions(auction_id)`
- `idx_product_name` su `completed_auctions(product_name)`
- `idx_completed_at` su `completed_auctions(completed_at)`
- `idx_won` su `completed_auctions(won)`
- `idx_username` su `bidder_performances(username)` [UNIQUE]
- `idx_win_rate` su `bidder_performances(win_rate)`
- `idx_product_key` su `product_statistics(product_key)` [UNIQUE]
- `idx_date` su `daily_metrics(date)` [UNIQUE]
### Ottimizzazioni
- Retry automatico su fallimenti (3 tentativi)
- Timeout comandi: 30 secondi
- Connection pooling gestito da Npgsql
- Transazioni ACID per consistenza dati
---
## ?? Roadmap
### Prossime Features
- [ ] **Auto-generazione Insights**: Analisi pattern vincenti automatica
- [ ] **Heatmap Competizione**: Orari migliori per puntare
- [ ] **ML Predictions**: Predizione probabilità vittoria
- [ ] **Alert System**: Notifiche su insight critici
- [ ] **Export Analytics**: CSV/Excel per analisi esterna
- [ ] **Backup Scheduler**: Backup automatici giornalieri
---
## ?? Riferimenti
- [Npgsql Documentation](https://www.npgsql.org/doc/)
- [EF Core PostgreSQL](https://www.npgsql.org/efcore/)
- [PostgreSQL 16 Docs](https://www.postgresql.org/docs/16/)
- [Docker PostgreSQL](https://hub.docker.com/_/postgres)
---
**Sistema PostgreSQL completamente integrato e pronto per analisi strategiche avanzate! ????**

View File

@@ -1,333 +0,0 @@
# ?? UI Sezione Database - Visual Guide
## ?? **Preview Sezione Configurazione Database**
### **Stato: PostgreSQL Abilitato**
```
???????????????????????????????????????????????????????????????????
? ?? Configurazione Database ?
???????????????????????????????????????????????????????????????????
? ?
? ?? Database Dual-Mode: ?
? ?
? AutoBidder utilizza PostgreSQL per statistiche avanzate ?
? e SQLite come fallback locale. Se PostgreSQL non è ?
? disponibile, le statistiche base continueranno a funzionare ?
? con SQLite. ?
? ?
???????????????????????????????????????????????????????????????????
? ?
? ?? [?] Usa PostgreSQL per Statistiche Avanzate ?
? Abilita analisi strategiche, raccomandazioni e metriche ?
? ?
? ?? PostgreSQL Connection String: ?
? ????????????????????????????????????????????????????????? ?
? ? Host=localhost;Port=5432;Database=autobidder_stats; ? ?
? ? Username=autobidder;Password=autobidder_password ? ?
? ????????????????????????????????????????????????????????? ?
? ?? Formato: Host=server;Port=5432;Database=dbname;... ?
? ?
? ?? [?] Auto-crea schema database se mancante ?
? Crea automaticamente le tabelle PostgreSQL al primo ?
? avvio ?
? ?
? ?? [?] Fallback automatico a SQLite se PostgreSQL non ?
? disponibile ?
? Consigliato: garantisce continuità anche senza ?
? PostgreSQL ?
? ?
? ?? Configurazione Docker: ?
? ?
? Se usi Docker Compose, il servizio PostgreSQL è già ?
? configurato. Usa: ?
? ?
? Host=postgres;Port=5432;Database=autobidder_stats; ?
? Username=autobidder;Password=${POSTGRES_PASSWORD} ?
? ?
? ?? Configura POSTGRES_PASSWORD nel file .env ?
? ?
? ???????????????????????????????????? ?
? ? ?? Test Connessione PostgreSQL ? ?
? ???????????????????????????????????? ?
? ?
? ? Connessione riuscita! PostgreSQL 16.1 ?
? ?
? ?????????????????????????????????????? ?
? ? ?? Salva Configurazione Database ? ?
? ?????????????????????????????????????? ?
? ?
???????????????????????????????????????????????????????????????????
```
---
### **Stato: PostgreSQL Disabilitato**
```
???????????????????????????????????????????????????????????????????
? ?? Configurazione Database ?
???????????????????????????????????????????????????????????????????
? ?
? ?? Database Dual-Mode: ?
? ?
? AutoBidder utilizza PostgreSQL per statistiche avanzate ?
? e SQLite come fallback locale. Se PostgreSQL non è ?
? disponibile, le statistiche base continueranno a funzionare ?
? con SQLite. ?
? ?
???????????????????????????????????????????????????????????????????
? ?
? ?? [ ] Usa PostgreSQL per Statistiche Avanzate ?
? Abilita analisi strategiche, raccomandazioni e metriche ?
? ?
? ?????????????????????????????????????? ?
? ? ?? Salva Configurazione Database ? ?
? ?????????????????????????????????????? ?
? ?
???????????????????????????????????????????????????????????????????
```
---
### **Test Connessione - Stati**
#### **In Corso**
```
????????????????????????????????????????
? ? Test in corso... ?
????????????????????????????????????????
```
#### **Successo**
```
????????????????????????????????????????
? ?? Test Connessione PostgreSQL ?
????????????????????????????????????????
? Connessione riuscita! PostgreSQL 16.1
```
#### **Errore - Host non raggiungibile**
```
????????????????????????????????????????
? ?? Test Connessione PostgreSQL ?
????????????????????????????????????????
? Errore PostgreSQL: No connection could be made because the target machine actively refused it
```
#### **Errore - Credenziali errate**
```
????????????????????????????????????????
? ?? Test Connessione PostgreSQL ?
????????????????????????????????????????
? Errore PostgreSQL: password authentication failed for user "autobidder"
```
#### **Errore - Database non esistente**
```
????????????????????????????????????????
? ?? Test Connessione PostgreSQL ?
????????????????????????????????????????
? Errore PostgreSQL: database "autobidder_stats" does not exist
```
---
## ?? **Stili CSS Applicati**
### **Card Container**
```css
.card {
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
```
### **Header**
```css
.card-header.bg-secondary {
background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%);
color: white;
border-bottom: none;
}
```
### **Alert Box**
```css
.alert-info {
background: linear-gradient(135deg, #d1ecf1 0%, #bee5eb 100%);
border: none;
border-left: 4px solid #17a2b8;
}
.alert-warning {
background: linear-gradient(135deg, #fff3cd 0%, #ffe69c 100%);
border: none;
border-left: 4px solid #ffc107;
}
```
### **Form Switch**
```css
.form-check-input:checked {
background-color: #0dcaf0;
border-color: #0dcaf0;
}
.form-switch .form-check-input {
width: 3em;
height: 1.5em;
}
```
### **Input Monospace**
```css
.font-monospace {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.9rem;
background: #f8f9fa;
border: 2px solid #dee2e6;
}
.font-monospace:focus {
border-color: #0dcaf0;
box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.25);
}
```
### **Button Hover**
```css
.btn.hover-lift {
transition: all 0.3s ease;
}
.btn.hover-lift:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.btn-primary.hover-lift:hover {
background: linear-gradient(135deg, #0d6efd 0%, #0b5ed7 100%);
}
```
### **Success/Error Feedback**
```css
.text-success {
color: #00d800 !important;
font-weight: 600;
}
.text-danger {
color: #f85149 !important;
font-weight: 600;
}
.bi-check-circle-fill,
.bi-x-circle-fill {
font-size: 1.2rem;
vertical-align: middle;
}
```
---
## ?? **Interazioni Utente**
### **Scenario 1: Prima Configurazione**
1. **Utente apre Settings** ? Vede sezione Database
2. **PostgreSQL disabilitato** ? Solo toggle visibile
3. **Utente abilita PostgreSQL** ? Si espandono opzioni
4. **Utente inserisce connection string** ? Formato validato
5. **Click "Test Connessione"** ? Spinner appare
6. **Test fallisce** ? ? Rosso con messaggio errore
7. **Utente corregge password** ? Riprova test
8. **Test successo** ? ? Verde con versione
9. **Click "Salva"** ? Alert "? Salvato!"
10. **Riavvio app** ? Settings caricati automaticamente
### **Scenario 2: Migrazione SQLite ? PostgreSQL**
1. **App funziona con SQLite** ? Dati locali
2. **Utente avvia PostgreSQL Docker** ? Container ready
3. **Utente va in Settings** ? Abilita PostgreSQL
4. **Connection string già compilata** ? Default localhost
5. **Test connessione** ? ? Successo
6. **Salva e riavvia** ? Program.cs crea tabelle
7. **Nuove aste registrate** ? Dati su PostgreSQL
8. **Vecchi dati SQLite** ? Rimangono intatti (fallback)
### **Scenario 3: Errore PostgreSQL**
1. **PostgreSQL configurato** ? App avviata
2. **Container PostgreSQL crash** ? Connection lost
3. **App rileva fallimento** ? Log: "PostgreSQL unavailable"
4. **Fallback automatico** ? "Using SQLite fallback"
5. **Statistiche continuano** ? Nessun downtime
6. **Utente ripristina PostgreSQL** ? Test connessione OK
7. **Riavvio app** ? Torna a usare PostgreSQL
---
## ?? **Responsive Design**
### **Desktop (>1200px)**
- Form a 2 colonne dove possibile
- Alert box con icone grandi
- Bottoni spaziati orizzontalmente
### **Tablet (768px-1200px)**
- Form a colonna singola
- Connection string full-width
- Bottoni stack verticale
### **Mobile (<768px)**
```
???????????????????????????
? ?? Configurazione DB ?
???????????????????????????
? ?? Info box ?
???????????????????????????
? ?? Usa PostgreSQL ?
? ?
? ?? Connection String: ?
? ??????????????????????? ?
? ? Host=... ? ?
? ??????????????????????? ?
? ?
? ?? Auto-create ?
? ?? Fallback SQLite ?
? ?
? [?? Test Connessione] ?
? ?
? ? Successo! ?
? ?
? [?? Salva] ?
???????????????????????????
```
---
## ?? **Accessibilità**
- ? **Keyboard Navigation**: Tab tra campi
- ? **Screen Readers**: Label descrittivi
- ? **Contrast Ratio**: WCAG AA compliant
- ? **Focus Indicators**: Visibili su tutti i controlli
- ? **Error Messages**: Chiari e specifici
- ? **Success Feedback**: Visivo + Alert
---
**UI completa, accessibile e user-friendly per configurazione PostgreSQL! ???**

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! ??

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!**

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!**

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!
```

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!**

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%!**

View File

@@ -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!**

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!**

View File

@@ -13,8 +13,9 @@ namespace AutoBidder.Models
{
/// <summary>
/// Numero massimo di righe di log da mantenere per ogni asta
/// Ridotto per ottimizzare consumo RAM
/// </summary>
private const int MAX_LOG_LINES = 500;
private const int MAX_LOG_LINES = 200;
public string AuctionId { get; set; } = "";
public string Name { get; set; } = ""; // Opzionale, può essere lasciato vuoto
@@ -65,10 +66,47 @@ namespace AutoBidder.Models
[JsonPropertyName("BidsUsedOnThisAuction")]
public int? BidsUsedOnThisAuction { get; set; }
// Timestamp
public DateTime AddedAt { get; set; } = DateTime.UtcNow;
public DateTime? LastClickAt { get; set; }
// ?? NUOVO: Sistema timing basato su deadline
/// <summary>
/// Timestamp UTC preciso della scadenza dell'asta.
/// Calcolato come: DateTime.UtcNow + Timer (quando riceviamo lo stato)
/// </summary>
[JsonIgnore]
public DateTime? DeadlineUtc { get; set; }
/// <summary>
/// Timestamp UTC dell'ultimo aggiornamento della deadline.
/// Usato per rilevare reset del timer.
/// </summary>
[JsonIgnore]
public DateTime? LastDeadlineUpdateUtc { get; set; }
/// <summary>
/// Timer raw dell'ultimo stato ricevuto (in secondi).
/// Usato per rilevare cambiamenti nel timer.
/// </summary>
[JsonIgnore]
public double LastRawTimer { get; set; }
/// <summary>
/// True se la puntata è già stata schedulata per questo ciclo.
/// Resettato quando il timer si resetta.
/// </summary>
[JsonIgnore]
public bool BidScheduled { get; set; }
/// <summary>
/// Timer per cui è stata schedulata l'ultima puntata.
/// Usato per evitare doppie puntate sullo stesso ciclo.
/// </summary>
[JsonIgnore]
public double LastScheduledTimerMs { get; set; }
// Storico
public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>();
public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase);
@@ -128,26 +166,333 @@ namespace AutoBidder.Models
[JsonIgnore]
public AuctionState? LastState { get; set; }
/// <summary>
/// Aggiunge una voce al log dell'asta con limite automatico di righe
/// Aggiunge una voce al log dell'asta con deduplicazione e limite automatico di righe.
/// Se il messaggio è identico all'ultimo, incrementa un contatore invece di duplicare.
/// </summary>
/// <param name="message">Messaggio da aggiungere al log</param>
/// <param name="maxLines">Numero massimo di righe da mantenere (default: 500)</param>
public void AddLog(string message, int maxLines = 500)
{
var entry = $"{DateTime.Now:HH:mm:ss.fff} - {message}";
var timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
// DEBUG: Print per verificare che i log vengano aggiunti
#if DEBUG
System.Diagnostics.Debug.WriteLine($"[AddLog] {AuctionId}: {message}");
#endif
// ?? DEDUPLICAZIONE: Se l'ultimo messaggio è uguale, incrementa contatore
if (AuctionLog.Count > 0)
{
var lastEntry = AuctionLog[^1]; // Ultimo elemento
// Estrai il messaggio senza timestamp e contatore
var lastMessageStart = lastEntry.IndexOf(" - ");
if (lastMessageStart > 0)
{
var lastMessage = lastEntry.Substring(lastMessageStart + 3);
// Rimuovi eventuale contatore esistente (es: " (x5)")
var counterMatch = System.Text.RegularExpressions.Regex.Match(lastMessage, @" \(x(\d+)\)$");
if (counterMatch.Success)
{
lastMessage = lastMessage.Substring(0, lastMessage.Length - counterMatch.Length);
}
// Se il messaggio è identico, aggiorna contatore
if (lastMessage == message)
{
int newCount = counterMatch.Success
? int.Parse(counterMatch.Groups[1].Value) + 1
: 2;
// Aggiorna l'ultimo entry con il nuovo contatore
AuctionLog[^1] = $"{timestamp} - {message} (x{newCount})";
return;
}
}
}
// Nuovo messaggio diverso dall'ultimo
var entry = $"{timestamp} - {message}";
AuctionLog.Add(entry);
// Mantieni solo gli ultimi maxLines log
if (AuctionLog.Count > maxLines)
{
// Rimuovi i log più vecchi per mantenere la dimensione sotto controllo
int excessCount = AuctionLog.Count - maxLines;
AuctionLog.RemoveRange(0, excessCount);
}
}
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 (ridotto per RAM)
/// </summary>
private const int MAX_LATENCY_HISTORY = 10;
/// <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;
// ???????????????????????????????????????????????????????????????????
// GESTIONE MEMORIA
// ???????????????????????????????????????????????????????????????????
/// <summary>
/// Pulisce tutti i dati in memoria dell'asta per liberare RAM.
/// Chiamare prima di rimuovere l'asta dalla lista.
/// </summary>
public void ClearData()
{
// Pulisci liste storiche
BidHistory?.Clear();
BidHistory = null!;
RecentBids?.Clear();
RecentBids = null!;
AuctionLog?.Clear();
AuctionLog = null!;
BidderStats?.Clear();
BidderStats = null!;
LatencyHistory?.Clear();
LatencyHistory = null!;
AggressiveBidders?.Clear();
AggressiveBidders = null!;
// Pulisci oggetti complessi
LastState = null;
CalculatedValue = null;
DuelOpponent = null;
WinLimitDescription = null;
// Reset flag
IsTrackedFromStart = false;
TrackingStartedAt = null;
DeadlineUtc = null;
LastDeadlineUpdateUtc = null;
}
/// <summary>
/// Compatta i dati mantenendo solo le informazioni recenti.
/// Utile per ridurre la memoria senza eliminare completamente i dati.
/// </summary>
public void CompactData(int maxBidHistory = 50, int maxRecentBids = 30, int maxLogLines = 100)
{
// Compatta BidHistory
if (BidHistory != null && BidHistory.Count > maxBidHistory)
{
var recent = BidHistory.TakeLast(maxBidHistory).ToList();
BidHistory.Clear();
BidHistory.AddRange(recent);
BidHistory.TrimExcess();
}
// Compatta RecentBids
if (RecentBids != null && RecentBids.Count > maxRecentBids)
{
var recent = RecentBids.TakeLast(maxRecentBids).ToList();
RecentBids.Clear();
RecentBids.AddRange(recent);
RecentBids.TrimExcess();
}
// Compatta AuctionLog
if (AuctionLog != null && AuctionLog.Count > maxLogLines)
{
var recent = AuctionLog.TakeLast(maxLogLines).ToList();
AuctionLog.Clear();
AuctionLog.AddRange(recent);
AuctionLog.TrimExcess();
}
// Compatta LatencyHistory
if (LatencyHistory != null && LatencyHistory.Count > 10)
{
var recent = LatencyHistory.TakeLast(10).ToList();
LatencyHistory.Clear();
LatencyHistory.AddRange(recent);
LatencyHistory.TrimExcess();
}
// Compatta BidderStats - mantieni solo i top bidders
if (BidderStats != null && BidderStats.Count > 20)
{
var topBidders = BidderStats
.OrderByDescending(kv => kv.Value.BidCount)
.Take(20)
.ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase);
BidderStats.Clear();
foreach (var kv in topBidders)
BidderStats[kv.Key] = kv.Value;
}
}
}
/// <summary>

View File

@@ -3,12 +3,25 @@ using System;
namespace AutoBidder.Models
{
/// <summary>
/// Informazioni su un utente che ha piazzato puntate
/// Informazioni su un utente che ha piazzato puntate.
/// Il conteggio è CUMULATIVO dall'inizio del monitoraggio (non limitato come RecentBids).
/// </summary>
public class BidderInfo
{
public string Username { get; set; } = "";
/// <summary>
/// Conteggio CUMULATIVO delle puntate dall'inizio del monitoraggio.
/// Questo valore non viene mai decrementato anche se RecentBids viene troncato.
/// </summary>
public int BidCount { get; set; } = 0;
/// <summary>
/// Conteggio puntate visibili nell'attuale finestra RecentBids (per UI).
/// Può essere inferiore a BidCount se RecentBids è stato troncato.
/// </summary>
public int RecentBidCount { get; set; } = 0;
public DateTime LastBidTime { get; set; } = DateTime.MinValue;
public string LastBidTimeDisplay => LastBidTime == DateTime.MinValue

View File

@@ -35,6 +35,14 @@ namespace AutoBidder.Models
public int? RecommendedMaxResets { get; set; }
public int? RecommendedMaxBids { get; set; }
// Valori di default definiti dall'utente (editabili)
public double? UserDefaultMinPrice { get; set; }
public double? UserDefaultMaxPrice { get; set; }
public int? UserDefaultMinResets { get; set; }
public int? UserDefaultMaxResets { get; set; }
public int? UserDefaultMaxBids { get; set; }
public int? UserDefaultBidBeforeDeadlineMs { get; set; }
// JSON con statistiche per fascia oraria
public string? HourlyStatsJson { get; set; }
@@ -117,4 +125,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;
}
}

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! ??

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.

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!** ??

View File

@@ -1,4 +1,4 @@
@page "/browser"
@page "/browser"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@using AutoBidder.Models
@using AutoBidder.Services
@@ -220,18 +220,18 @@
<h6 class="auction-name" title="@auction.Name">@auction.Name</h6>
<div class="auction-price">
<span class="current-price">@auction.CurrentPrice.ToString("0.00") €</span>
<span class="current-price">@auction.CurrentPrice.ToString("0.00") €</span>
@if (auction.BuyNowPrice > 0)
{
<span class="buynow-price text-muted">
<small>Compra: @auction.BuyNowPrice.ToString("0.00") €</small>
<small>Compra: @auction.BuyNowPrice.ToString("0.00") €</small>
</span>
}
</div>
<div class="auction-bidder">
<i class="bi bi-person-fill text-muted me-1"></i>
<span>@(string.IsNullOrEmpty(auction.LastBidder) ? "—" : auction.LastBidder)</span>
<span>@(string.IsNullOrEmpty(auction.LastBidder) ? "—" : auction.LastBidder)</span>
</div>
<div class="auction-timer @(auction.RemainingSeconds <= 3 ? "urgent" : "")">
@@ -295,7 +295,14 @@
private List<BidooCategoryInfo> categories = new();
private List<BidooBrowserAuction> auctions = new();
private List<BidooBrowserAuction> filteredAuctions = new();
private int selectedCategoryIndex = 0;
// 🔥 Usa stato persistente da AppState
private int selectedCategoryIndex
{
get => AppState.BrowserCategoryIndex;
set => AppState.BrowserCategoryIndex = value;
}
private int currentPage = 0;
private bool isLoading = false;
@@ -303,8 +310,12 @@ private bool isLoadingMore = false;
private bool canLoadMore = true;
private string? errorMessage = null;
// ? NUOVO: Ricerca
private string searchQuery = "";
// 🔥 Usa stato persistente per la ricerca
private string searchQuery
{
get => AppState.BrowserSearchQuery;
set => AppState.BrowserSearchQuery = value;
}
private System.Threading.Timer? stateUpdateTimer;
private CancellationTokenSource? cts;
@@ -314,9 +325,20 @@ private bool isUpdatingInBackground = false;
{
await LoadCategories();
// 🔥 Se c'è una categoria salvata, carica le aste
if (categories.Count > 0)
{
await LoadAuctions();
// Se selectedCategoryIndex è valido, carica quella categoria
if (selectedCategoryIndex >= 0 && selectedCategoryIndex < categories.Count)
{
await LoadAuctions();
}
else
{
// Altrimenti carica la prima categoria
selectedCategoryIndex = 0;
await LoadAuctions();
}
}
// Auto-update states every 500ms for real-time price updates
@@ -532,9 +554,30 @@ private bool isUpdatingInBackground = false;
{
if (browserAuction.IsMonitored) return;
// ?? Carica impostazioni di default
// 🔥 Carica impostazioni di default
var settings = AutoBidder.Utilities.SettingsManager.Load();
// 🔥 Determina stato iniziale da impostazioni
bool isActive = false;
bool isPaused = false;
switch (settings.DefaultNewAuctionState)
{
case "Active":
isActive = true;
isPaused = false;
break;
case "Paused":
isActive = true;
isPaused = true;
break;
case "Stopped":
default:
isActive = false;
isPaused = false;
break;
}
var auctionInfo = new AuctionInfo
{
AuctionId = browserAuction.AuctionId,
@@ -542,7 +585,7 @@ private bool isUpdatingInBackground = false;
OriginalUrl = browserAuction.Url,
BuyNowPrice = (double)browserAuction.BuyNowPrice,
// ?? FIX: Applica valori dalle impostazioni
// 🔥 Applica valori dalle impostazioni
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs,
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
MinPrice = settings.DefaultMinPrice,
@@ -550,8 +593,9 @@ private bool isUpdatingInBackground = false;
MinResets = settings.DefaultMinResets,
MaxResets = settings.DefaultMaxResets,
IsActive = true,
IsPaused = true, // Start paused
// 🔥 Usa stato da impostazioni invece di hardcoded
IsActive = isActive,
IsPaused = isPaused,
AddedAt = DateTime.UtcNow
};
@@ -565,7 +609,7 @@ private bool isUpdatingInBackground = false;
// Save to disk
AutoBidder.Utilities.PersistenceManager.SaveAuctions(AppState.Auctions.ToList());
// ?? FIX CRITICO: Avvia il monitor se non è già attivo
// ?? FIX CRITICO: Avvia il monitor se non è già attivo
if (!AppState.IsMonitoringActive)
{
AuctionMonitor.Start();

File diff suppressed because it is too large Load Diff

View File

@@ -15,21 +15,83 @@ namespace AutoBidder.Pages
[Inject] private HtmlCacheService HtmlCache { get; set; } = default!;
[Inject] private StatsService StatsService { get; set; } = default!;
private List<AuctionInfo> auctions => AppState.Auctions.ToList();
// Cache locale per evitare ricreare liste ad ogni render
private List<AuctionInfo>? _cachedAuctions = new List<AuctionInfo>(); // Inizializzata per evitare NullRef
private int _lastAuctionsHash;
private List<AuctionInfo> auctions
{
get
{
try
{
// Protezione null-safety
if (AppState == null) return _cachedAuctions ?? new List<AuctionInfo>();
// Usa cache per evitare copie continue
var current = AppState.GetAuctionsDirectRef();
if (current == null) return _cachedAuctions ?? new List<AuctionInfo>();
var hash = current.Count;
if (_cachedAuctions == null || hash != _lastAuctionsHash)
{
_cachedAuctions = current.ToList();
_lastAuctionsHash = hash;
}
return _cachedAuctions;
}
catch
{
// Fallback sicuro
return _cachedAuctions ?? new List<AuctionInfo>();
}
}
}
// Invalida cache quando necessario
private void InvalidateAuctionCache()
{
_cachedAuctions = null;
}
private AuctionInfo? selectedAuction
{
get => AppState.SelectedAuction;
set => AppState.SelectedAuction = value;
get
{
// ?? FIX CRITICO: Ottieni sempre il riferimento dalla lista originale
// Questo assicura che le modifiche ai campi vengano salvate correttamente
var selected = AppState.SelectedAuction;
if (selected != null)
{
var liveReference = AppState.GetAuctionById(selected.AuctionId);
return liveReference;
}
return null;
}
set
{
AppState.SelectedAuction = value;
}
}
private List<LogEntry> globalLog => AppState.GlobalLog.ToList();
private List<LogEntry> globalLog => AppState.GetLogDirectRef();
private bool isMonitoringActive
{
get => AppState.IsMonitoringActive;
set => AppState.IsMonitoringActive = value;
}
private System.Threading.Timer? refreshTimer;
private System.Threading.Timer? sessionTimer;
private System.Threading.Timer? logRefreshTimer;
private DateTime _lastUiUpdate = DateTime.MinValue;
private DateTime _lastLogRefresh = DateTime.MinValue;
private const int UI_UPDATE_THROTTLE_MS = 250; // Max 4 aggiornamenti al secondo
private const int LOG_REFRESH_MS = 500; // Aggiorna log ogni 500ms se ci sono cambiamenti
// Tracking log asta selezionata
private int _lastSelectedAuctionLogCount = 0;
private string? _lastSelectedAuctionId = null;
// Dialog Aggiungi Asta
private bool showAddDialog = false;
@@ -46,6 +108,14 @@ 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;
// ?? Sorting griglia aste
private string auctionSortColumn = "nome";
private bool auctionSortAscending = true;
protected override void OnInitialized()
{
@@ -67,20 +137,68 @@ namespace AutoBidder.Pages
AuctionMonitor.OnLog += OnGlobalLog;
AuctionMonitor.OnAuctionUpdated += OnAuctionUpdated;
refreshTimer = new System.Threading.Timer(async _ =>
// RIMOSSO: Timer refresh ogni 1s causava lag estremo
// Gli aggiornamenti UI ora avvengono SOLO quando necessario via eventi
// Timer dedicato per aggiornare i log (leggero, solo se ci sono cambiamenti)
logRefreshTimer = new System.Threading.Timer(async _ =>
{
await InvokeAsync(StateHasChanged);
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
try
{
bool needsRefresh = false;
// Controlla se il log globale è cambiato
var currentGlobalLogCount = globalLog.Count;
if (currentGlobalLogCount != lastLogCount)
{
lastLogCount = currentGlobalLogCount;
needsRefresh = true;
}
// Controlla se l'asta selezionata ha nuovi log
var selected = selectedAuction;
if (selected != null)
{
var currentAuctionLogCount = selected.AuctionLog.Count;
// Se è cambiata l'asta selezionata, forza refresh
if (_lastSelectedAuctionId != selected.AuctionId)
{
_lastSelectedAuctionId = selected.AuctionId;
_lastSelectedAuctionLogCount = currentAuctionLogCount;
needsRefresh = true;
}
// Se il count dei log è cambiato, refresh
else if (currentAuctionLogCount != _lastSelectedAuctionLogCount)
{
_lastSelectedAuctionLogCount = currentAuctionLogCount;
needsRefresh = true;
}
}
else
{
// Nessuna asta selezionata, reset tracking
_lastSelectedAuctionId = null;
_lastSelectedAuctionLogCount = 0;
}
if (needsRefresh)
{
await InvokeAsync(StateHasChanged);
}
}
catch { /* Ignora errori del timer */ }
}, null, TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(300));
// Carica sessione all'avvio
LoadSession();
// Timer per aggiornamento sessione ogni 30 secondi
// Timer per aggiornamento sessione ogni 60 secondi (era 30)
sessionTimer = new System.Threading.Timer(async _ =>
{
await RefreshSessionAsync();
await InvokeAsync(StateHasChanged);
}, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
await ThrottledStateHasChanged();
}, null, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));
}
protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -91,11 +209,36 @@ 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
private async Task OnAppStateChangedAsync()
{
await ThrottledStateHasChanged();
}
/// <summary>
/// Aggiorna UI con throttling per evitare troppi re-render
/// </summary>
private async Task ThrottledStateHasChanged()
{
var now = DateTime.UtcNow;
if ((now - _lastUiUpdate).TotalMilliseconds < UI_UPDATE_THROTTLE_MS)
{
return; // Skip, aggiornamento recente già fatto
}
_lastUiUpdate = now;
await InvokeAsync(StateHasChanged);
}
@@ -122,7 +265,8 @@ namespace AutoBidder.Pages
private void SaveAuctions()
{
AutoBidder.Utilities.PersistenceManager.SaveAuctions(auctions);
// ?? FIX: Usa il metodo dedicato che salva la lista originale, non una copia
AppState.PersistAuctions();
AddLog("Aste salvate");
}
@@ -134,12 +278,12 @@ namespace AutoBidder.Pages
private void OnGlobalLog(string message)
{
AppState.AddLog($"[{DateTime.Now:HH:mm:ss}] {message}");
InvokeAsync(StateHasChanged);
// Non forziamo StateHasChanged qui - verrà aggiornato dal throttle
}
private void OnAuctionUpdated(AuctionState state)
{
var auction = auctions.FirstOrDefault(a => a.AuctionId == state.AuctionId);
var auction = AppState.GetAuctionById(state.AuctionId);
if (auction != null)
{
// Salva l'ultimo stato ricevuto
@@ -151,18 +295,21 @@ namespace AutoBidder.Pages
auction.BidsUsedOnThisAuction = state.MyBidsCount.Value;
}
// Notifica il cambiamento usando InvokeAsync per thread-safety
_ = InvokeAsync(() =>
{
AppState.ForceUpdate();
StateHasChanged();
});
// Invalida cache
InvalidateAuctionCache();
// Notifica con throttling
_ = ThrottledStateHasChanged();
}
}
private void SelectAuction(AuctionInfo auction)
{
selectedAuction = auction;
// Imposta direttamente senza notifiche async per risposta immediata
AppState.SetSelectedAuctionDirect(auction);
// Forza aggiornamento immediato per visualizzare i log dell'asta selezionata
StateHasChanged();
}
// Gestione controlli globali
@@ -284,6 +431,11 @@ namespace AutoBidder.Pages
{
auction.BidsUsedOnThisAuction = result.BidsUsedOnThisAuction.Value;
}
else
{
// Incrementa contatore locale se server non risponde
auction.BidsUsedOnThisAuction = (auction.BidsUsedOnThisAuction ?? 0) + 1;
}
SaveAuctions();
}
@@ -462,6 +614,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 +720,185 @@ 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}");
}
}
/// <summary>
/// Rimuove tutte le aste terminate (salvandole prima nel database)
/// </summary>
private async Task RemoveCompletedAuctions()
{
var completedAuctions = auctions.Where(a => !a.IsActive).ToList();
if (completedAuctions.Count == 0)
{
await JSRuntime.InvokeVoidAsync("alert", "Nessuna asta terminata da rimuovere.");
return;
}
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm",
$"Rimuovere {completedAuctions.Count} aste terminate?\n\n" +
"? Verranno salvate automaticamente nelle statistiche.");
if (!confirmed) return;
try
{
int removed = 0;
foreach (var auction in completedAuctions)
{
// RemoveAuction salva automaticamente l'asta nel database se terminata
AuctionMonitor.RemoveAuction(auction.AuctionId);
AppState.RemoveAuction(auction);
removed++;
}
// Deseleziona se l'asta selezionata era tra quelle rimosse
if (selectedAuction != null && !selectedAuction.IsActive)
{
selectedAuction = null;
}
SaveAuctions();
AddLog($"[CLEANUP] Rimosse {removed} aste terminate");
await JSRuntime.InvokeVoidAsync("alert", $"? Rimosse {removed} aste terminate.\nSono state salvate nelle statistiche.");
}
catch (Exception ex)
{
AddLog($"Errore rimozione aste terminate: {ex.Message}");
await JSRuntime.InvokeVoidAsync("alert", $"Errore:\n{ex.Message}");
}
}
/// <summary>
/// Verifica se ci sono aste terminate
/// </summary>
private bool HasCompletedAuctions()
{
return auctions.Any(a => !a.IsActive);
}
// ???????????????????????????????????????????????????????????????????
// RIMOZIONE ASTE PER STATO
// ???????????????????????????????????????????????????????????????????
private async Task RemoveActiveAuctions()
{
await RemoveAuctionsByCondition(
a => a.IsActive && !a.IsPaused && (a.LastState == null || a.LastState.Status == AuctionStatus.Running),
"attive",
GetActiveAuctionsCount()
);
}
private async Task RemovePausedAuctions()
{
await RemoveAuctionsByCondition(
a => a.IsPaused || (a.LastState != null && a.LastState.Status == AuctionStatus.Paused),
"in pausa",
GetPausedAuctionsCount()
);
}
private async Task RemoveStoppedAuctions()
{
await RemoveAuctionsByCondition(
a => !a.IsActive && (a.LastState == null || (a.LastState.Status != AuctionStatus.EndedWon && a.LastState.Status != AuctionStatus.EndedLost)),
"fermate",
GetStoppedAuctionsCount()
);
}
private async Task RemoveWonAuctions()
{
await RemoveAuctionsByCondition(
a => a.LastState != null && a.LastState.Status == AuctionStatus.EndedWon,
"vinte",
GetWonAuctionsCount()
);
}
private async Task RemoveLostAuctions()
{
await RemoveAuctionsByCondition(
a => a.LastState != null && a.LastState.Status == AuctionStatus.EndedLost,
"perse",
GetLostAuctionsCount()
);
}
private async Task RemoveAuctionsByCondition(Func<AuctionInfo, bool> condition, string stateLabel, int count)
{
if (count == 0) return;
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm",
$"Rimuovere {count} aste {stateLabel}?");
if (!confirmed) return;
try
{
var toRemove = auctions.Where(condition).ToList();
int removed = 0;
foreach (var auction in toRemove)
{
AuctionMonitor.RemoveAuction(auction.AuctionId);
AppState.RemoveAuction(auction);
removed++;
}
if (selectedAuction != null && condition(selectedAuction))
{
selectedAuction = null;
}
SaveAuctions();
AddLog($"[CLEANUP] Rimosse {removed} aste {stateLabel}");
}
catch (Exception ex)
{
AddLog($"Errore rimozione: {ex.Message}");
}
}
// ???????????????????????????????????????????????????????????????????
// SPLITTER RESIZE (gestito via JS)
// ???????????????????????????????????????????????????????????????????
private async Task RemoveSelectedAuctionWithConfirm()
{
if (selectedAuction == null) return;
@@ -711,7 +1048,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 +1077,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 +1184,62 @@ namespace AutoBidder.Pages
}
}
private string GetCurrentUsername()
{
return sessionUsername ?? "";
}
// ?? SORTING GRIGLIA ASTE
private void SortAuctionsBy(string column)
{
if (auctionSortColumn == column)
{
auctionSortAscending = !auctionSortAscending;
}
else
{
auctionSortColumn = column;
auctionSortAscending = true;
}
}
private MarkupString GetSortIndicator(string column)
{
if (auctionSortColumn != column) return new MarkupString("");
return new MarkupString(auctionSortAscending ? " ?" : " ?");
}
private IEnumerable<AuctionInfo> GetSortedAuctions()
{
var list = auctions.AsEnumerable();
list = auctionSortColumn switch
{
"stato" => auctionSortAscending
? list.OrderBy(a => a.IsActive).ThenBy(a => a.IsPaused)
: list.OrderByDescending(a => a.IsActive).ThenByDescending(a => a.IsPaused),
"nome" => auctionSortAscending
? list.OrderBy(a => a.Name)
: list.OrderByDescending(a => a.Name),
"prezzo" => auctionSortAscending
? list.OrderBy(a => a.LastState?.Price ?? 0)
: list.OrderByDescending(a => a.LastState?.Price ?? 0),
"timer" => auctionSortAscending
? list.OrderBy(a => a.LastState?.Timer ?? 999)
: list.OrderByDescending(a => a.LastState?.Timer ?? 999),
"puntate" => auctionSortAscending
? list.OrderBy(a => a.BidsUsedOnThisAuction ?? 0)
: list.OrderByDescending(a => a.BidsUsedOnThisAuction ?? 0),
"ping" => auctionSortAscending
? list.OrderBy(a => a.PollingLatencyMs)
: list.OrderByDescending(a => a.PollingLatencyMs),
_ => list
};
return list;
}
// ?? NUOVI METODI: Visualizzazione valori prodotto
private string GetTotalCostDisplay(AuctionInfo? auction)
@@ -957,11 +1348,6 @@ namespace AutoBidder.Pages
var latency = auction.PollingLatencyMs;
if (latency <= 0) return "-";
// Colora in base al ping
var cssClass = latency < 100 ? "text-success" :
latency < 300 ? "text-warning" :
"text-danger";
return $"{latency}ms";
}
catch
@@ -969,6 +1355,25 @@ namespace AutoBidder.Pages
return "-";
}
}
private string GetPingClass(AuctionInfo? auction)
{
try
{
if (auction == null) return "text-muted";
var latency = auction.PollingLatencyMs;
if (latency <= 0) return "text-muted";
if (latency < 100) return "text-success";
if (latency < 300) return "text-warning";
return "text-danger";
}
catch
{
return "text-muted";
}
}
private IEnumerable<string> GetAuctionLog(AuctionInfo auction)
{
@@ -1089,8 +1494,8 @@ namespace AutoBidder.Pages
public void Dispose()
{
refreshTimer?.Dispose();
sessionTimer?.Dispose();
logRefreshTimer?.Dispose();
// Rimuovi sottoscrizioni (ASYNC)
if (AppState != null)
@@ -1173,5 +1578,76 @@ namespace AutoBidder.Pages
StateHasChanged();
}
}
// ???????????????????????????????????????????????????????????????????
// METODI CONTEGGIO STATO ASTE
// ???????????????????????????????????????????????????????????????????
private int GetActiveAuctionsCount()
{
try
{
return auctions?.Count(a => a.IsActive && !a.IsPaused &&
(a.LastState == null || a.LastState.Status == AuctionStatus.Running)) ?? 0;
}
catch
{
return 0;
}
}
private int GetPausedAuctionsCount()
{
try
{
return auctions?.Count(a => a.IsPaused ||
(a.LastState != null && a.LastState.Status == AuctionStatus.Paused)) ?? 0;
}
catch
{
return 0;
}
}
private int GetWonAuctionsCount()
{
try
{
return auctions?.Count(a => a.LastState != null &&
a.LastState.Status == AuctionStatus.EndedWon) ?? 0;
}
catch
{
return 0;
}
}
private int GetLostAuctionsCount()
{
try
{
return auctions?.Count(a => a.LastState != null &&
a.LastState.Status == AuctionStatus.EndedLost) ?? 0;
}
catch
{
return 0;
}
}
private int GetStoppedAuctionsCount()
{
try
{
return auctions?.Count(a => !a.IsActive &&
(a.LastState == null ||
(a.LastState.Status != AuctionStatus.EndedWon &&
a.LastState.Status != AuctionStatus.EndedLost))) ?? 0;
}
catch
{
return 0;
}
}
}
}

View File

@@ -133,43 +133,126 @@
</h2>
<div id="collapse-defaults" class="accordion-collapse collapse" aria-labelledby="heading-defaults" data-bs-parent="#settingsAccordion">
<div class="accordion-body">
<div class="row g-3">
<div class="alert alert-info border-0 shadow-sm mb-3">
<i class="bi bi-info-circle me-2"></i>
Usa i pulsanti <i class="bi bi-arrow-repeat"></i> per applicare la singola impostazione a tutte le aste
</div>
<!-- TICKER LOOP - TIMING -->
<h6 class="fw-bold mb-3 text-primary"><i class="bi bi-clock-history"></i> Sistema di Timing (Ticker Loop)</h6>
<div class="alert alert-warning border-0 shadow-sm mb-3">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Importante:</strong> Il sistema rispetta ESATTAMENTE i valori inseriti.
Se la puntata arriva tardi, aumenta l'"Anticipo puntata". Non vengono applicate compensazioni automatiche.
</div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-speedometer2"></i> Anticipo puntata (ms)</label>
<input type="number" class="form-control" @bind="settings.DefaultBidBeforeDeadlineMs" />
<div class="input-group">
<input type="number" class="form-control" @bind="settings.DefaultBidBeforeDeadlineMs" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultBidBeforeDeadlineMs))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultBidBeforeDeadlineMs))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
<div class="form-text">Millisecondi prima della scadenza per tentare la puntata</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-hourglass-split"></i> Intervallo Ticker (ms)</label>
<input type="number" class="form-control" @bind="settings.TickerIntervalMs" min="10" max="500" />
<div class="form-text">Più basso = più preciso ma più CPU. Consigliato: 50-100ms</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-funnel"></i> Soglia controllo strategie (ms)</label>
<input type="number" class="form-control" @bind="settings.StrategyCheckThresholdMs" />
<div class="form-text">Inizia a verificare le strategie solo sotto questa soglia (ottimizza CPU)</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="lateBidWarning" @bind="settings.ShowLateBidWarning" />
<label class="form-check-label" for="lateBidWarning">
<strong>Mostra avviso puntata tardiva</strong>
<div class="form-text">Suggerisce di aumentare l'anticipo se la puntata arriva tardi</div>
</label>
</div>
</div>
</div>
<hr class="my-4" />
<h6 class="fw-bold mb-3"><i class="bi bi-sliders"></i> Limiti Default</h6>
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-hand-index-thumb"></i> Click massimi</label>
<input type="number" class="form-control" @bind="settings.DefaultMaxClicks" />
<div class="input-group">
<input type="number" class="form-control" @bind="settings.DefaultMaxClicks" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMaxClicks))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultMaxClicks))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
<div class="form-text">0 = illimitati</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo minimo (€)</label>
<input type="number" step="0.01" class="form-control" @bind="settings.DefaultMinPrice" />
<div class="input-group">
<input type="number" step="0.01" class="form-control" @bind="settings.DefaultMinPrice" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMinPrice))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultMinPrice))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo massimo (€)</label>
<input type="number" step="0.01" class="form-control" @bind="settings.DefaultMaxPrice" />
<div class="input-group">
<input type="number" step="0.01" class="form-control" @bind="settings.DefaultMaxPrice" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMaxPrice))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultMaxPrice))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-arrow-repeat"></i> Reset minimi</label>
<input type="number" class="form-control" @bind="settings.DefaultMinResets" />
<div class="input-group">
<input type="number" class="form-control" @bind="settings.DefaultMinResets" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMinResets))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultMinResets))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-arrow-repeat"></i> Reset massimi</label>
<input type="number" class="form-control" @bind="settings.DefaultMaxResets" />
<div class="input-group">
<input type="number" class="form-control" @bind="settings.DefaultMaxResets" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMaxResets))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultMaxResets))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
<div class="col-12 col-md-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 class="form-text">Questa è un'impostazione globale</div>
</div>
</div>
@if (!string.IsNullOrEmpty(singleSettingMessage))
{
<div class="alert alert-success mt-3 mb-0 fade-in">
<i class="bi bi-check-circle me-2"></i>@singleSettingMessage
</div>
}
<div class="mt-3">
<button class="btn btn-success" @onclick="SaveSettings"><i class="bi bi-check-lg"></i> Salva</button>
@@ -178,25 +261,314 @@
</div>
</div>
<!-- LIMITI LOG -->
<!-- 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">
<div class="alert alert-info border-0 shadow-sm mb-4">
<i class="bi bi-info-circle me-2"></i>
<strong>Nota:</strong> Le strategie decidono <strong>SE</strong> puntare, non <strong>QUANDO</strong>.
Il timing è controllato solo dall'impostazione "Anticipo puntata" nelle Impostazioni Predefinite.
</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-cash-coin"></i> Controllo Convenienza</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="valueCheck" @bind="settings.ValueCheckEnabled" />
<label class="form-check-label" for="valueCheck">
<strong>Blocca puntate non convenienti</strong>
<div class="form-text">Blocca se il costo totale supera il prezzo "Compra Subito" oltre la soglia</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label">Risparmio minimo richiesto (%)</label>
<input type="number" step="0.1" class="form-control" @bind="settings.MinSavingsPercentage" />
<div class="form-text">
Negativo = tolleranza perdita. Es: -5 = permetti fino al 5% di perdita<br />
Positivo = richiede risparmio. Es: 10 = richiedi almeno 10% di risparmio
</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>
<!-- LOGGING GRANULARE -->
<div class="accordion-item">
<h2 class="accordion-header" id="heading-logs">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-logs" aria-expanded="false" aria-controls="collapse-logs">
<i class="bi bi-journal-text me-2"></i> Limiti Log
<i class="bi bi-journal-text me-2"></i> Logging e Limiti
</button>
</h2>
<div id="collapse-logs" class="accordion-collapse collapse" aria-labelledby="heading-logs" data-bs-parent="#settingsAccordion">
<div class="accordion-body">
<div class="row g-3">
<h6 class="fw-bold mb-3"><i class="bi bi-filter"></i> Cosa Loggare</h6>
<p class="text-muted small mb-3">Scegli quali informazioni visualizzare nei log delle aste.</p>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logBids" @bind="settings.LogBids" />
<label class="form-check-label" for="logBids">
<strong>[BID] Puntate piazzate</strong>
<div class="form-text">Log quando viene effettuata una puntata</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logStrategy" @bind="settings.LogStrategyDecisions" />
<label class="form-check-label" for="logStrategy">
<strong>[STRATEGY] Decisioni strategie</strong>
<div class="form-text">Log quando una strategia blocca la puntata</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logStatus" @bind="settings.LogAuctionStatus" />
<label class="form-check-label" for="logStatus">
<strong>[STATUS] Stato asta</strong>
<div class="form-text">Log quando l'asta termina, si resetta, ecc.</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logErrors" @bind="settings.LogErrors" />
<label class="form-check-label" for="logErrors">
<strong>[ERROR/WARN] Errori e warning</strong>
<div class="form-text">Log errori e avvisi importanti</div>
</label>
</div>
</div>
</div>
<h6 class="fw-bold mb-3"><i class="bi bi-bug"></i> Debug Avanzato</h6>
<div class="alert alert-warning border-0 shadow-sm mb-3">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Attenzione:</strong> Queste opzioni generano molti log. Attiva solo per debug!
</div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logTiming" @bind="settings.LogTiming" />
<label class="form-check-label" for="logTiming">
<strong>[TIMING] Timing puntate</strong>
<div class="form-text">Log dettagliato timing (molto verbose!)</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logValue" @bind="settings.LogValueCalculations" />
<label class="form-check-label" for="logValue">
<strong>[VALUE] Calcoli valore</strong>
<div class="form-text">Log calcoli convenienza prodotto</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logCompetition" @bind="settings.LogCompetition" />
<label class="form-check-label" for="logCompetition">
<strong>[COMPETITION] Competizione</strong>
<div class="form-text">Log rilevamento competizione e heat</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="logOpponent" @bind="settings.LogOpponentProfiling" />
<label class="form-check-label" for="logOpponent">
<strong>[OPPONENT] Profiling avversari</strong>
<div class="form-text">Log analisi bidder aggressivi</div>
</label>
</div>
</div>
</div>
<h6 class="fw-bold mb-3"><i class="bi bi-sliders"></i> Limiti</h6>
<div class="row g-3">
<div class="col-12 col-md-4">
<label class="form-label fw-bold"><i class="bi bi-list-ul"></i> Righe log globale</label>
<input type="number" class="form-control" @bind="settings.MaxGlobalLogLines" />
</div>
<div class="col-12 col-md-6">
<div class="col-12 col-md-4">
<label class="form-label fw-bold"><i class="bi bi-list-check"></i> Righe log per asta</label>
<input type="number" class="form-control" @bind="settings.MaxLogLinesPerAuction" />
</div>
<div class="col-12 col-md-6">
<div class="col-12 col-md-4">
<label class="form-label fw-bold"><i class="bi bi-clock-history"></i> Voci storia puntate</label>
<input type="number" class="form-control" @bind="settings.MaxBidHistoryEntries" />
</div>
@@ -334,7 +706,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>
@@ -406,6 +778,21 @@
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.925rem;
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
@code {
@@ -418,6 +805,15 @@ 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;
// Applica singole impostazioni
private HashSet<string> applyingSettings = new();
private string? singleSettingMessage = null;
private AutoBidder.Utilities.AppSettings settings = new();
private System.Threading.Timer? updateTimer;
@@ -437,6 +833,131 @@ 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 async Task ApplySingleSettingToAll(string settingName)
{
applyingSettings.Add(settingName);
singleSettingMessage = null;
StateHasChanged();
try
{
// Prima salva le impostazioni
SaveSettings();
var auctions = AuctionMonitor.GetAuctions().ToList();
int count = 0;
string settingLabel = "";
foreach (var auction in auctions)
{
switch (settingName)
{
case nameof(settings.DefaultBidBeforeDeadlineMs):
auction.BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs;
settingLabel = $"Anticipo puntata ({settings.DefaultBidBeforeDeadlineMs}ms)";
break;
case nameof(settings.DefaultMaxClicks):
// MaxClicks viene applicato tramite MaxBidsOverride
auction.MaxBidsOverride = settings.DefaultMaxClicks > 0 ? settings.DefaultMaxClicks : null;
settingLabel = $"Click massimi ({(settings.DefaultMaxClicks > 0 ? settings.DefaultMaxClicks.ToString() : "illimitati")})";
break;
case nameof(settings.DefaultMinPrice):
auction.MinPrice = settings.DefaultMinPrice;
settingLabel = $"Prezzo minimo (€{settings.DefaultMinPrice:F2})";
break;
case nameof(settings.DefaultMaxPrice):
auction.MaxPrice = settings.DefaultMaxPrice;
settingLabel = $"Prezzo massimo (€{settings.DefaultMaxPrice:F2})";
break;
case nameof(settings.DefaultMinResets):
auction.MinResets = settings.DefaultMinResets;
settingLabel = $"Reset minimi ({settings.DefaultMinResets})";
break;
case nameof(settings.DefaultMaxResets):
auction.MaxResets = settings.DefaultMaxResets;
settingLabel = $"Reset massimi ({settings.DefaultMaxResets})";
break;
}
count++;
}
// Salva le aste modificate
AutoBidder.Utilities.PersistenceManager.SaveAuctions(auctions);
singleSettingMessage = $"? {settingLabel} applicato a {count} aste!";
}
catch (Exception ex)
{
singleSettingMessage = $"Errore: {ex.Message}";
}
finally
{
applyingSettings.Remove(settingName);
StateHasChanged();
// Rimuovi messaggio dopo 3 secondi
await Task.Delay(3000);
singleSettingMessage = null;
StateHasChanged();
}
}
private void SyncStartupSelectionsFromSettings()
{
if (settings.RememberAuctionStates)

File diff suppressed because it is too large Load Diff

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()));
@@ -549,4 +551,48 @@ app.MapRazorPages(); // ? AGGIUNTO: abilita Razor Pages (Login, Logout)
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
// ?????????????????????????????????????????????????????????????????
// TIMER PULIZIA MEMORIA PERIODICA
// ?????????????????????????????????????????????????????????????????
// Timer per pulizia periodica della memoria (ogni 5 minuti)
var memoryCleanupTimer = new System.Threading.Timer(async _ =>
{
try
{
using var scope = app.Services.CreateScope();
var appState = scope.ServiceProvider.GetRequiredService<ApplicationStateService>();
var htmlCache = scope.ServiceProvider.GetRequiredService<HtmlCacheService>();
// Pulisci cache HTML scaduta
htmlCache.CleanExpiredCache();
// Compatta dati aste completate
appState.CleanupCompletedAuctions();
// Forza garbage collection leggera
GC.Collect(1, GCCollectionMode.Optimized, false);
// Log statistiche memoria
var stats = appState.GetMemoryStats();
var memoryMB = GC.GetTotalMemory(false) / 1024.0 / 1024.0;
Console.WriteLine($"[MEMORY] Cleanup: {stats.AuctionsCount} aste, " +
$"{stats.TotalBidHistoryEntries} bid history, " +
$"{stats.TotalRecentBidsEntries} recent bids, " +
$"{stats.GlobalLogEntries} global log, " +
$"RAM: {memoryMB:F1}MB");
}
catch (Exception ex)
{
Console.WriteLine($"[MEMORY ERROR] Cleanup failed: {ex.Message}");
}
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
// Assicura che il timer venga disposto quando l'app si chiude
app.Lifetime.ApplicationStopping.Register(() =>
{
Console.WriteLine("[SHUTDOWN] Disposing memory cleanup timer...");
memoryCleanupTimer.Dispose();
});
app.Run();

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!**

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! ??

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!** ??

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

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!**

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!**

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.

View File

@@ -1,4 +1,5 @@
using AutoBidder.Models;
using AutoBidder.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -52,6 +53,67 @@ namespace AutoBidder.Services
}
}
/// <summary>
/// Ottiene riferimento diretto alla lista per lettura veloce (NO COPY).
/// ATTENZIONE: Non modificare la lista, usare solo per lettura!
/// </summary>
public List<AuctionInfo> GetAuctionsDirectRef()
{
return _auctions; // Accesso diretto senza lock per velocità
}
/// <summary>
/// Ottiene riferimento diretto al log per lettura veloce (NO COPY).
/// </summary>
public List<LogEntry> GetLogDirectRef()
{
return _globalLog;
}
/// <summary>
/// Imposta l'asta selezionata SENZA notificare eventi async.
/// Usare per risposta UI immediata.
/// </summary>
public void SetSelectedAuctionDirect(AuctionInfo? auction)
{
_selectedAuction = auction;
}
/// <summary>
/// Ottiene la lista originale delle aste per il salvataggio.
/// ATTENZIONE: Usare solo per persistenza, non per iterazione durante modifiche!
/// </summary>
public List<AuctionInfo> GetAuctionsForPersistence()
{
lock (_lock)
{
return _auctions;
}
}
/// <summary>
/// Forza il salvataggio delle aste correnti su disco.
/// </summary>
public void PersistAuctions()
{
lock (_lock)
{
AutoBidder.Utilities.PersistenceManager.SaveAuctions(_auctions);
}
}
/// <summary>
/// Ottiene l'asta modificabile per ID.
/// IMPORTANTE: Dopo modifiche, chiamare PersistAuctions() per salvare!
/// </summary>
public AuctionInfo? GetAuctionById(string auctionId)
{
lock (_lock)
{
return _auctions.FirstOrDefault(a => a.AuctionId == auctionId);
}
}
public AuctionInfo? SelectedAuction
{
get
@@ -112,6 +174,47 @@ namespace AutoBidder.Services
}
}
// === STATO AUCTION BROWSER ===
private int _browserCategoryIndex = 0;
private string _browserSearchQuery = "";
public int BrowserCategoryIndex
{
get
{
lock (_lock)
{
return _browserCategoryIndex;
}
}
set
{
lock (_lock)
{
_browserCategoryIndex = value;
}
}
}
public string BrowserSearchQuery
{
get
{
lock (_lock)
{
return _browserSearchQuery;
}
}
set
{
lock (_lock)
{
_browserSearchQuery = value;
}
}
}
// === METODI GESTIONE ASTE ===
public void SetAuctions(List<AuctionInfo> auctions)
@@ -200,15 +303,16 @@ namespace AutoBidder.Services
{
_globalLog.Add(entry);
// Mantieni solo gli ultimi 1000 log
if (_globalLog.Count > 1000)
// Mantieni solo gli ultimi 500 log (ridotto da 1000 per RAM)
if (_globalLog.Count > 500)
{
_globalLog.RemoveRange(0, _globalLog.Count - 1000);
_globalLog.RemoveRange(0, _globalLog.Count - 500);
_globalLog.TrimExcess();
}
}
_ = NotifyLogAddedAsync(message);
_ = NotifyStateChangedAsync();
// RIMOSSO: NotifyStateChangedAsync qui causava troppi re-render
// I log vengono visualizzati al prossimo refresh naturale
}
public void ClearLog()
@@ -314,6 +418,80 @@ namespace AutoBidder.Services
{
_ = NotifyStateChangedAsync();
}
// ???????????????????????????????????????????????????????????????????
// GESTIONE MEMORIA
// ???????????????????????????????????????????????????????????????????
/// <summary>
/// Compatta i dati di tutte le aste per ridurre il consumo RAM
/// </summary>
public void CompactAllAuctions()
{
lock (_lock)
{
foreach (var auction in _auctions)
{
try
{
auction.CompactData();
}
catch { /* Ignora errori */ }
}
}
Console.WriteLine($"[AppState] Compattati dati di {_auctions.Count} aste");
}
/// <summary>
/// Pulisce i dati delle aste terminate dalla memoria
/// </summary>
public void CleanupCompletedAuctions()
{
lock (_lock)
{
foreach (var auction in _auctions.Where(a => !a.IsActive))
{
try
{
// Per le aste terminate, mantieni solo dati essenziali
auction.CompactData(maxBidHistory: 20, maxRecentBids: 10, maxLogLines: 50);
}
catch { }
}
}
}
/// <summary>
/// Ritorna statistiche sull'uso della memoria
/// </summary>
public MemoryStats GetMemoryStats()
{
lock (_lock)
{
return new MemoryStats
{
AuctionsCount = _auctions.Count,
ActiveAuctionsCount = _auctions.Count(a => a.IsActive),
TotalBidHistoryEntries = _auctions.Sum(a => a.BidHistory?.Count ?? 0),
TotalRecentBidsEntries = _auctions.Sum(a => a.RecentBids?.Count ?? 0),
TotalLogEntries = _auctions.Sum(a => a.AuctionLog?.Count ?? 0),
GlobalLogEntries = _globalLog.Count
};
}
}
}
/// <summary>
/// Statistiche memoria per debug
/// </summary>
public class MemoryStats
{
public int AuctionsCount { get; set; }
public int ActiveAuctionsCount { get; set; }
public int TotalBidHistoryEntries { get; set; }
public int TotalRecentBidsEntries { get; set; }
public int TotalLogEntries { get; set; }
public int GlobalLogEntries { get; set; }
}
/// <summary>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,544 @@
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>
/// 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;
}
// ? RIMOSSO: Entry Point - Era sbagliato!
// I limiti MinPrice/MaxPrice impostati dall'utente sono RIGIDI.
// Se l'utente imposta MaxPrice=2€, vuole puntare FINO A 2€, non fino al 70%!
// I controlli MinPrice/MaxPrice sono già gestiti in AuctionMonitor.ShouldBid()
// L'Entry Point può essere usato SOLO per calcolare limiti CONSIGLIATI, non per bloccare.
// ?? 1. ANTI-BOT - Rileva pattern bot (timing identico)
if (settings.AntiBotDetectionEnabled && !string.IsNullOrEmpty(state.LastBidder))
{
var botCheck = DetectBotPattern(auction, state.LastBidder, currentUsername);
if (botCheck.IsBot)
{
decision.ShouldBid = false;
decision.Reason = $"Anti-bot: {state.LastBidder} pattern sospetto (var={botCheck.TimingVarianceMs:F0}ms)";
return decision;
}
}
// ?? 2. USER EXHAUSTION - Sfrutta utenti stanchi (info solo, non blocca)
if (settings.UserExhaustionEnabled && !string.IsNullOrEmpty(state.LastBidder))
{
var exhaustionCheck = CheckUserExhaustion(auction, state.LastBidder, currentUsername);
// Non blocchiamo, ma potremmo loggare per info
}
// 3. 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;
}
}
// ? RIMOSSO: DetectLastSecondSniper - causava falsi positivi
// In un duello, TUTTI i bidder hanno pattern regolari (ogni reset del timer)
// Questa strategia bloccava puntate legittime e faceva perdere aste
// ?? 7. STRATEGIA: Price Momentum (con soglia più alta)
// Se il prezzo sta salendo TROPPO velocemente, pausa
var priceVelocity = CalculatePriceVelocity(auction);
if (priceVelocity > 0.10) // +10 centesimi/secondo = MOLTO veloce
{
decision.ShouldBid = false;
decision.Reason = $"Prezzo sale troppo veloce ({priceVelocity:F3}€/s)";
return decision;
}
return decision;
}
/// <summary>
/// Calcola la velocità di crescita del prezzo (€/secondo)
/// </summary>
private double CalculatePriceVelocity(AuctionInfo auction)
{
if (auction.RecentBids.Count < 5) return 0;
var recentBids = auction.RecentBids.Take(10).ToList();
if (recentBids.Count < 2) return 0;
var first = recentBids.Last();
var last = recentBids.First();
var timeDiffSeconds = last.Timestamp - first.Timestamp;
if (timeDiffSeconds <= 0) return 0;
var priceDiff = last.Price - first.Price;
return (double)priceDiff / timeDiffSeconds;
}
/// <summary>
/// Rileva pattern bot analizzando i delta timing degli ultimi bid
/// </summary>
private (bool IsBot, double TimingVarianceMs) DetectBotPattern(AuctionInfo auction, string? lastBidder, string currentUsername)
{
if (string.IsNullOrEmpty(lastBidder) || lastBidder.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
return (false, 999);
// Ottieni gli ultimi 3+ bid di questo utente
var userBids = auction.RecentBids
.Where(b => b.Username.Equals(lastBidder, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(b => b.Timestamp)
.Take(4)
.ToList();
if (userBids.Count < 3)
return (false, 999);
// Calcola i delta tra bid consecutivi
var deltas = new List<long>();
for (int i = 0; i < userBids.Count - 1; i++)
{
deltas.Add(userBids[i].Timestamp - userBids[i + 1].Timestamp);
}
if (deltas.Count < 2)
return (false, 999);
// Calcola varianza dei delta
var avg = deltas.Average();
var variance = deltas.Sum(d => Math.Pow(d - avg, 2)) / deltas.Count;
var stdDev = Math.Sqrt(variance) * 1000; // Converti in ms
// Se la varianza è < 50ms, probabilmente è un bot
return (stdDev < 50, stdDev);
}
/// <summary>
/// Verifica se un utente è esausto (molte puntate, può mollare)
/// </summary>
private (bool ShouldExploit, string Reason) CheckUserExhaustion(AuctionInfo auction, string? lastBidder, string currentUsername)
{
if (string.IsNullOrEmpty(lastBidder) || lastBidder.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
return (false, "");
// Verifica se l'utente è un "heavy user" (>50 puntate totali)
if (auction.BidderStats.TryGetValue(lastBidder, out var stats))
{
if (stats.BidCount > 50)
{
// Se ci sono pochi altri bidder attivi, può essere un buon momento
var activeBidders = auction.BidderStats.Values.Count(b => b.BidCount > 5);
if (activeBidders <= 3)
{
return (true, $"{lastBidder} ha {stats.BidCount} puntate, potrebbe mollare");
}
}
}
return (false, "");
}
/// <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; }
}
}

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 con +
name = name
.Replace("&plus;", "+")
.Replace("&amp;plus;", "+")
.Replace("&amp;", "&"); // Decodifica & residui
auction.Name = name;
}
// Estrai prezzo compralo subito

View File

@@ -596,6 +596,157 @@ 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();
}),
new Migration(15, "Add user-defined default limits to ProductStatistics", async (conn) => {
var sql = @"
-- Aggiungi colonne per limiti definiti dall'utente (separati dai calcolati)
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMinPrice REAL;
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMaxPrice REAL;
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMinResets INTEGER;
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMaxResets INTEGER;
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMaxBids INTEGER;
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultBidBeforeDeadlineMs INTEGER;
";
await using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync();
})
};
@@ -1088,6 +1239,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>
@@ -1100,12 +1422,14 @@ namespace AutoBidder.Services
AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated)
VALUES (@productKey, @productName, @totalAuctions, @wonAuctions, @lostAuctions,
@avgFinalPrice, @minFinalPrice, @maxFinalPrice,
@avgBidsToWin, @minBidsToWin, @maxBidsToWin,
@avgResets, @minResets, @maxResets,
@recMinPrice, @recMaxPrice, @recMinResets, @recMaxResets, @recMaxBids,
@userMinPrice, @userMaxPrice, @userMinResets, @userMaxResets, @userMaxBids, @userBidDeadline,
@hourlyJson, @lastUpdated)
ON CONFLICT(ProductKey) DO UPDATE SET
ProductName = @productName,
@@ -1126,6 +1450,12 @@ namespace AutoBidder.Services
RecommendedMinResets = @recMinResets,
RecommendedMaxResets = @recMaxResets,
RecommendedMaxBids = @recMaxBids,
UserDefaultMinPrice = COALESCE(@userMinPrice, UserDefaultMinPrice),
UserDefaultMaxPrice = COALESCE(@userMaxPrice, UserDefaultMaxPrice),
UserDefaultMinResets = COALESCE(@userMinResets, UserDefaultMinResets),
UserDefaultMaxResets = COALESCE(@userMaxResets, UserDefaultMaxResets),
UserDefaultMaxBids = COALESCE(@userMaxBids, UserDefaultMaxBids),
UserDefaultBidBeforeDeadlineMs = COALESCE(@userBidDeadline, UserDefaultBidBeforeDeadlineMs),
HourlyStatsJson = @hourlyJson,
LastUpdated = @lastUpdated;
";
@@ -1150,6 +1480,12 @@ namespace AutoBidder.Services
new SqliteParameter("@recMinResets", (object?)stats.RecommendedMinResets ?? DBNull.Value),
new SqliteParameter("@recMaxResets", (object?)stats.RecommendedMaxResets ?? DBNull.Value),
new SqliteParameter("@recMaxBids", (object?)stats.RecommendedMaxBids ?? DBNull.Value),
new SqliteParameter("@userMinPrice", (object?)stats.UserDefaultMinPrice ?? DBNull.Value),
new SqliteParameter("@userMaxPrice", (object?)stats.UserDefaultMaxPrice ?? DBNull.Value),
new SqliteParameter("@userMinResets", (object?)stats.UserDefaultMinResets ?? DBNull.Value),
new SqliteParameter("@userMaxResets", (object?)stats.UserDefaultMaxResets ?? DBNull.Value),
new SqliteParameter("@userMaxBids", (object?)stats.UserDefaultMaxBids ?? DBNull.Value),
new SqliteParameter("@userBidDeadline", (object?)stats.UserDefaultBidBeforeDeadlineMs ?? DBNull.Value),
new SqliteParameter("@hourlyJson", (object?)stats.HourlyStatsJson ?? DBNull.Value),
new SqliteParameter("@lastUpdated", DateTime.UtcNow.ToString("O"))
);
@@ -1166,6 +1502,7 @@ namespace AutoBidder.Services
AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated
FROM ProductStatistics
WHERE ProductKey = @productKey;
@@ -1200,8 +1537,14 @@ namespace AutoBidder.Services
RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16),
RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18),
HourlyStatsJson = reader.IsDBNull(19) ? null : reader.GetString(19),
LastUpdated = reader.GetString(20)
UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19),
UserDefaultMaxPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20),
UserDefaultMinResets = reader.IsDBNull(21) ? null : reader.GetInt32(21),
UserDefaultMaxResets = reader.IsDBNull(22) ? null : reader.GetInt32(22),
UserDefaultMaxBids = reader.IsDBNull(23) ? null : reader.GetInt32(23),
UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(24) ? null : reader.GetInt32(24),
HourlyStatsJson = reader.IsDBNull(25) ? null : reader.GetString(25),
LastUpdated = reader.GetString(26)
};
}
@@ -1251,6 +1594,7 @@ namespace AutoBidder.Services
AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated
FROM ProductStatistics
ORDER BY TotalAuctions DESC;
@@ -1286,13 +1630,66 @@ namespace AutoBidder.Services
RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16),
RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18),
HourlyStatsJson = reader.IsDBNull(19) ? null : reader.GetString(19),
LastUpdated = reader.GetString(20)
UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19),
UserDefaultMaxPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20),
UserDefaultMinResets = reader.IsDBNull(21) ? null : reader.GetInt32(21),
UserDefaultMaxResets = reader.IsDBNull(22) ? null : reader.GetInt32(22),
UserDefaultMaxBids = reader.IsDBNull(23) ? null : reader.GetInt32(23),
UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(24) ? null : reader.GetInt32(24),
HourlyStatsJson = reader.IsDBNull(25) ? null : reader.GetString(25),
LastUpdated = reader.GetString(26)
});
}
return results;
}
/// <summary>
/// Elimina un prodotto dalle statistiche per ProductKey
/// </summary>
public async Task<int> DeleteProductStatisticsAsync(string productKey)
{
var sql = @"
DELETE FROM ProductStatistics
WHERE ProductKey = @productKey;
";
return await ExecuteNonQueryAsync(sql,
new SqliteParameter("@productKey", productKey)
);
}
/// <summary>
/// Aggiorna i valori di default definiti dall'utente per un prodotto
/// </summary>
public async Task UpdateProductUserDefaultsAsync(string productKey,
double? minPrice, double? maxPrice,
int? minResets, int? maxResets,
int? maxBids, int? bidBeforeDeadlineMs)
{
var sql = @"
UPDATE ProductStatistics
SET UserDefaultMinPrice = @minPrice,
UserDefaultMaxPrice = @maxPrice,
UserDefaultMinResets = @minResets,
UserDefaultMaxResets = @maxResets,
UserDefaultMaxBids = @maxBids,
UserDefaultBidBeforeDeadlineMs = @bidDeadline,
LastUpdated = @lastUpdated
WHERE ProductKey = @productKey;
";
await ExecuteNonQueryAsync(sql,
new SqliteParameter("@productKey", productKey),
new SqliteParameter("@minPrice", (object?)minPrice ?? DBNull.Value),
new SqliteParameter("@maxPrice", (object?)maxPrice ?? DBNull.Value),
new SqliteParameter("@minResets", (object?)minResets ?? DBNull.Value),
new SqliteParameter("@maxResets", (object?)maxResets ?? DBNull.Value),
new SqliteParameter("@maxBids", (object?)maxBids ?? DBNull.Value),
new SqliteParameter("@bidDeadline", (object?)bidBeforeDeadlineMs ?? DBNull.Value),
new SqliteParameter("@lastUpdated", DateTime.UtcNow.ToString("O"))
);
}
private AuctionResultExtended ReadAuctionResultExtended(Microsoft.Data.Sqlite.SqliteDataReader reader)
{

View File

@@ -28,6 +28,7 @@ namespace AutoBidder.Services
private readonly int _maxConcurrentRequests;
private readonly TimeSpan _cacheExpiration;
private readonly int _maxRetries;
private readonly int _maxCacheEntries;
// Logging callback
public Action<string>? OnLog { get; set; }
@@ -36,12 +37,14 @@ namespace AutoBidder.Services
int maxConcurrentRequests = 3,
int requestsPerSecond = 5,
TimeSpan? cacheExpiration = null,
int maxRetries = 2)
int maxRetries = 2,
int maxCacheEntries = 50)
{
_maxConcurrentRequests = maxConcurrentRequests;
_minRequestDelay = TimeSpan.FromMilliseconds(1000.0 / requestsPerSecond);
_cacheExpiration = cacheExpiration ?? TimeSpan.FromMinutes(5);
_cacheExpiration = cacheExpiration ?? TimeSpan.FromMinutes(3); // Ridotto da 5 a 3 minuti
_maxRetries = maxRetries;
_maxCacheEntries = maxCacheEntries;
_rateLimiter = new SemaphoreSlim(maxConcurrentRequests, maxConcurrentRequests);
_httpClient.Timeout = TimeSpan.FromSeconds(15);
@@ -191,10 +194,26 @@ namespace AutoBidder.Services
}
/// <summary>
/// Salva HTML in cache
/// Salva HTML in cache con limite dimensione
/// </summary>
private void SaveToCache(string url, string html)
{
// Limita dimensione cache per evitare memory leak
if (_cache.Count >= _maxCacheEntries)
{
// Rimuovi le entry più vecchie
var oldestEntries = _cache
.OrderBy(kvp => kvp.Value.Timestamp)
.Take(_cache.Count - _maxCacheEntries + 10) // Rimuovi 10 extra per evitare chiamate frequenti
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in oldestEntries)
{
_cache.TryRemove(key, out _);
}
}
_cache[url] = new CachedHtml
{
Html = html,

View File

@@ -1,6 +1,8 @@
@using Microsoft.AspNetCore.Components.Authorization
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject AuctionMonitor AuctionMonitor
@implements IDisposable
<div class="nav-sidebar">
<div class="nav-header">
@@ -36,11 +38,32 @@
</div>
<div class="nav-footer">
<!-- Info Sessione Utente -->
@if (!string.IsNullOrEmpty(sessionUsername))
{
<div class="session-stats">
<div class="session-stat">
<i class="bi bi-hand-index-thumb-fill"></i>
<div class="stat-content">
<span class="stat-label">Puntate</span>
<span class="stat-value @GetBidsClass()">@sessionRemainingBids</span>
</div>
</div>
<div class="session-stat">
<i class="bi bi-wallet2"></i>
<div class="stat-content">
<span class="stat-label">Credito</span>
<span class="stat-value text-success">€@sessionShopCredit.ToString("F2")</span>
</div>
</div>
</div>
}
<AuthorizeView>
<Authorized>
<div class="user-badge">
<div class="user-badge @(string.IsNullOrEmpty(sessionUsername) ? "disconnected" : "connected")">
<i class="bi bi-person-circle"></i>
<span>@context.User.Identity?.Name</span>
<span>@(string.IsNullOrEmpty(sessionUsername) ? "Non connesso" : sessionUsername)</span>
</div>
<a href="/Account/Logout" class="nav-menu-item logout-item">
<i class="bi bi-box-arrow-right"></i>
@@ -52,6 +75,52 @@
</nav>
</div>
@code {
private string? sessionUsername;
private int sessionRemainingBids;
private double sessionShopCredit;
private System.Threading.Timer? refreshTimer;
protected override void OnInitialized()
{
LoadSessionInfo();
// Refresh ogni 5 secondi
refreshTimer = new System.Threading.Timer(async _ =>
{
LoadSessionInfo();
await InvokeAsync(StateHasChanged);
}, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
}
private void LoadSessionInfo()
{
try
{
var session = AuctionMonitor.GetSession();
if (session != null)
{
sessionUsername = session.Username;
sessionRemainingBids = session.RemainingBids;
sessionShopCredit = session.ShopCredit;
}
}
catch { }
}
private string GetBidsClass()
{
if (sessionRemainingBids <= 10) return "text-danger";
if (sessionRemainingBids <= 50) return "text-warning";
return "text-success";
}
public void Dispose()
{
refreshTimer?.Dispose();
}
}
<style>
.nav-sidebar {
display: flex;
@@ -150,6 +219,52 @@
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.session-stats {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.75rem;
margin-bottom: 0.75rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.session-stat {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.375rem 0;
}
.session-stat i {
font-size: 0.875rem;
width: 1.25rem;
text-align: center;
color: rgba(255, 255, 255, 0.5);
}
.session-stat .stat-content {
display: flex;
justify-content: space-between;
flex: 1;
align-items: center;
}
.session-stat .stat-label {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
}
.session-stat .stat-value {
font-size: 0.875rem;
font-weight: 600;
}
.session-stat .text-success { color: #22c55e; }
.session-stat .text-warning { color: #f59e0b; }
.session-stat .text-danger { color: #ef4444; }
.user-badge {
display: flex;
align-items: center;
@@ -162,6 +277,15 @@
font-size: 0.875rem;
}
.user-badge.connected {
border-left: 3px solid #22c55e;
}
.user-badge.disconnected {
border-left: 3px solid #ef4444;
color: rgba(255, 255, 255, 0.4);
}
.user-badge i {
font-size: 1.25rem;
}

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!**

View File

@@ -41,7 +41,7 @@ namespace AutoBidder.Utilities
}
// Calcola risparmio rispetto al prezzo "Compra Subito"
if (auctionInfo.BuyNowPrice.HasValue)
if (auctionInfo.BuyNowPrice.HasValue && auctionInfo.BuyNowPrice.Value > 0)
{
var buyNowTotal = auctionInfo.BuyNowPrice.Value;
if (auctionInfo.ShippingCost.HasValue)
@@ -50,12 +50,24 @@ namespace AutoBidder.Utilities
}
value.Savings = buyNowTotal - value.TotalCostIfWin;
value.SavingsPercentage = (value.Savings.Value / buyNowTotal) * 100.0;
// ?? FIX: Evita divisione per zero
if (buyNowTotal > 0)
{
value.SavingsPercentage = (value.Savings.Value / buyNowTotal) * 100.0;
}
else
{
// Se il buyNowTotal è 0, imposta un valore fittizio negativo per indicare perdita
value.SavingsPercentage = -100.0;
}
value.IsWorthIt = value.Savings.Value > 0;
}
else
{
// Senza prezzo "Compra Subito", consideriamo sempre conveniente
// Senza prezzo "Compra Subito" valido, consideriamo sempre conveniente
// Questo permette di puntare su aste senza dati di riferimento
value.IsWorthIt = true;
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Text.Json;
@@ -7,7 +7,7 @@ namespace AutoBidder.Utilities
public class AppSettings
{
// NUOVE IMPOSTAZIONI PREDEFINITE PER LE ASTE
public int DefaultBidBeforeDeadlineMs { get; set; } = 200;
public int DefaultBidBeforeDeadlineMs { get; set; } = 800;
public bool DefaultCheckAuctionOpenBeforeBid { get; set; } = false;
public double DefaultMinPrice { get; set; } = 0;
public double DefaultMaxPrice { get; set; } = 0;
@@ -15,6 +15,33 @@ namespace AutoBidder.Utilities
public int DefaultMinResets { get; set; } = 0;
public int DefaultMaxResets { get; set; } = 0;
// ═══════════════════════════════════════════════════════════════════
// TICKER LOOP - SISTEMA DI TIMING SEMPLIFICATO
// ═══════════════════════════════════════════════════════════════════
/// <summary>
/// Intervallo del ticker in millisecondi.
/// Più basso = più preciso ma più CPU.
/// Valori consigliati: 50-100ms
/// Default: 50ms
/// </summary>
public int TickerIntervalMs { get; set; } = 50;
/// <summary>
/// Soglia in millisecondi per iniziare i controlli delle strategie.
/// Se il timer è superiore a questo valore, non vengono eseguiti i controlli.
/// Questo ottimizza le risorse evitando controlli inutili quando siamo lontani dal momento di puntare.
/// Default: 5000ms (5 secondi)
/// </summary>
public int StrategyCheckThresholdMs { get; set; } = 5000;
/// <summary>
/// Mostra avviso quando una puntata arriva troppo tardi (timer scaduto).
/// Suggerisce all'utente di aumentare il tempo di puntata.
/// Default: true
/// </summary>
public bool ShowLateBidWarning { get; set; } = true;
// LIMITI LOG
/// <summary>
/// Numero massimo di righe di log da mantenere per ogni singola asta (default: 500)
@@ -49,7 +76,7 @@ namespace AutoBidder.Utilities
// ? NUOVO: LIMITE MINIMO PUNTATE
/// <summary>
/// Numero minimo di puntate residue da mantenere sull'account.
/// Se impostato > 0, il sistema non punterà se le puntate residue scenderebbero sotto questa soglia.
/// Se impostato > 0, il sistema non punterà se le puntate residue scenderebbero sotto questa soglia.
/// Default: 0 (nessun limite)
/// </summary>
public int MinimumRemainingBids { get; set; } = 0;
@@ -89,16 +116,311 @@ namespace AutoBidder.Utilities
/// <summary>
/// Esegue pulizia automatica record incompleti all'avvio.
/// Default: false (può rimuovere dati utili in caso di errori temporanei)
/// Default: false (può rimuovere dati utili in caso di errori temporanei)
/// </summary>
public bool DatabaseAutoCleanupIncomplete { get; set; } = false;
/// <summary>
/// Numero massimo di giorni da mantenere nei risultati aste.
/// Record più vecchi vengono eliminati automaticamente.
/// Record più vecchi vengono eliminati automaticamente.
/// Default: 180 (6 mesi), 0 = disabilitato
/// </summary>
public int DatabaseMaxRetentionDays { get; set; } = 180;
// ???????????????????????????????????????????????????????????????
// STRATEGIE AVANZATE DI PUNTATA
// ???????????????????????????????????????????????????????????????
// ❌ RIMOSSO: Jitter, Offset Dinamico, Latenza Adattiva
// Il timing è gestito SOLO da DefaultBidBeforeDeadlineMs
// Le strategie decidono SE puntare, non QUANDO
// 🎯 LOGGING GRANULARE
// ???????????????????????????????????????????????????????????????
/// <summary>
/// Log quando viene piazzata una puntata [BID]
/// Default: true
/// </summary>
public bool LogBids { get; set; } = true;
/// <summary>
/// Log quando una strategia blocca la puntata [STRATEGY]
/// Default: true
/// </summary>
public bool LogStrategyDecisions { get; set; } = true;
/// <summary>
/// Log calcoli valore prodotto [VALUE]
/// Default: false (attiva per debug)
/// </summary>
public bool LogValueCalculations { get; set; } = false;
/// <summary>
/// Log rilevamento competizione e heat [COMPETITION]
/// Default: false
/// </summary>
public bool LogCompetition { get; set; } = false;
/// <summary>
/// Log timing e polling (molto verbose!) [TIMING]
/// Default: false (attiva solo per debug timing)
/// </summary>
public bool LogTiming { get; set; } = false;
/// <summary>
/// Log errori e warning [ERROR/WARN]
/// Default: true
/// </summary>
public bool LogErrors { get; set; } = true;
/// <summary>
/// Applica automaticamente i limiti salvati nel prodotto quando si aggiunge una nuova asta.
/// Se TRUE e il prodotto ha valori di default salvati, li applica automaticamente.
/// Default: true (consigliato per coerenza)
/// </summary>
public bool AutoApplyProductDefaults { get; set; } = true;
/// <summary>
/// Log stato asta (terminata, reset, ecc.) [STATUS]
/// Default: true
/// </summary>
public bool LogAuctionStatus { get; set; } = true;
/// <summary>
/// Log profiling avversari [OPPONENT]
/// Default: false
/// </summary>
public bool LogOpponentProfiling { get; set; } = false;
// 🎯 STRATEGIE SEMPLIFICATE
/// <summary>
/// Entry Point: Usato SOLO per calcolare i limiti consigliati (70% del MaxPrice storico).
/// NON blocca le puntate! I limiti MinPrice/MaxPrice impostati dall'utente sono RIGIDI.
/// Default: true (per calcolo limiti consigliati)
/// </summary>
public bool EntryPointEnabled { get; set; } = true;
/// <summary>
/// Anti-Bot: Rileva pattern bot (timing identico con varianza minore di 50ms)
/// e evita di competere contro bot automatici.
/// Default: true
/// </summary>
public bool AntiBotDetectionEnabled { get; set; } = true;
/// <summary>
/// User Exhaustion: Sfrutta utenti stanchi (oltre 50 puntate)
/// quando ci sono pochi altri bidder attivi.
/// Default: true
/// </summary>
public bool UserExhaustionEnabled { get; set; } = true;
// 🎯 CONTROLLO CONVENIENZA PRODOTTO
/// <summary>
/// Abilita il controllo di convenienza basato sul valore del prodotto.
/// Se attivo, blocca le puntate quando il costo totale supera il prezzo "Compra Subito"
/// di una percentuale superiore a MinSavingsPercentage.
/// Default: true
/// </summary>
public bool ValueCheckEnabled { get; set; } = true;
/// <summary>
/// Percentuale minima di risparmio richiesta per continuare a puntare.
/// Valori negativi = tolleranza alla perdita.
/// Es: -5 = permetti fino al 5% di perdita rispetto al "Compra Subito"
/// 0 = blocca se costa uguale o più del "Compra Subito"
/// 10 = richiedi almeno 10% di risparmio
/// Default: -5 (permetti fino al 5% di perdita)
/// </summary>
public double MinSavingsPercentage { get; set; } = -5.0;
/// <summary>
/// Abilita il controllo anti-collisione hardcoded.
/// Se attivo, blocca le puntate quando ci sono 3+ bidder attivi negli ultimi 10 secondi.
/// ATTENZIONE: Questo controllo può far perdere aste competitive!
/// Default: false (DISABILITATO - non blocca mai)
/// </summary>
public bool HardcodedAntiCollisionEnabled { 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

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!

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

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!**

View File

@@ -299,17 +299,22 @@
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
/* ?? RIMOSSO: hover-lift causava movimento fastidioso */
.hover-lift:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
/* transform: translateY(-4px); - RIMOSSO */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background-color: rgba(255, 255, 255, 0.05);
}
/* ?? RIMOSSO: hover-scale causava zoom fastidioso */
.hover-scale {
transition: transform 0.3s ease;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.hover-scale:hover {
transform: scale(1.05);
/* transform: scale(1.05); - RIMOSSO */
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(13, 110, 253, 0.5);
}
.hover-rotate {
@@ -412,8 +417,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 +437,7 @@
transition: all 0.3s ease;
}
.badge:hover {
transform: scale(1.1);
/* Rimosso effetto scale su badge hover */
}
.badge-pulse {

View File

@@ -585,55 +585,67 @@ body {
.btn-success {
background: var(--success-color);
color: white;
transition: filter 0.2s ease, box-shadow 0.2s ease;
}
.btn-success:hover:not(:disabled) {
background: #059669;
filter: brightness(1.1);
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
}
.btn-warning {
background: var(--warning-color);
color: white;
transition: filter 0.2s ease, box-shadow 0.2s ease;
}
.btn-warning:hover:not(:disabled) {
background: #d97706;
filter: brightness(1.1);
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.3);
}
.btn-danger {
background: var(--danger-color);
color: white;
transition: filter 0.2s ease, box-shadow 0.2s ease;
}
.btn-danger:hover:not(:disabled) {
background: #dc2626;
filter: brightness(1.1);
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
}
.btn-primary {
background: var(--primary-color);
color: white;
transition: filter 0.2s ease, box-shadow 0.2s ease;
}
.btn-primary:hover:not(:disabled) {
background: #0284c7;
filter: brightness(1.1);
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.3);
}
.btn-secondary {
background: var(--bg-hover);
color: var(--text-secondary);
transition: filter 0.2s ease, box-shadow 0.2s ease;
}
.btn-secondary:hover:not(:disabled) {
background: var(--text-muted);
filter: brightness(1.15);
box-shadow: 0 2px 8px rgba(100, 116, 139, 0.2);
}
.btn-info {
background: var(--info-color);
color: white;
transition: filter 0.2s ease, box-shadow 0.2s ease;
}
.btn-info:hover:not(:disabled) {
background: #2563eb;
filter: brightness(1.1);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.btn:disabled {

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 */
@@ -703,8 +714,9 @@ main {
height: 100%;
}
/* 🔥 COMPATTATO: Ridotto padding per massimizzare spazio */
.tab-panel-content {
padding: 1rem;
padding: 0.5rem 0.75rem;
}
/* === GRADIENTS FOR CARDS === */
@@ -872,24 +884,78 @@ main {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 0.75rem;
margin: 0.5rem;
padding: 0.5rem;
margin: 0.25rem;
}
/* 🔥 COMPATTATO: Ridotto margin e padding per info-group */
.info-group {
margin-bottom: 0.75rem;
margin-bottom: 0.4rem;
}
.info-group label {
display: block;
font-weight: 600;
margin-bottom: 0.25rem;
margin-bottom: 0.15rem;
color: var(--text-secondary);
font-size: 0.813rem;
font-size: 0.75rem;
}
/* 🔥 COMPATTATO: Input più piccoli */
.info-group input.form-control,
.info-group select.form-control {
padding: 0.25rem 0.5rem;
font-size: 0.85rem;
height: auto;
}
/* 🔥 GRIGLIA IMPOSTAZIONI COMPATTA */
.settings-grid-compact {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.settings-grid-compact .setting-item {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.settings-grid-compact .setting-item label {
font-size: 0.7rem;
color: var(--text-secondary);
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.settings-grid-compact .setting-item label i {
margin-right: 0.2rem;
}
/* 🔥 Input stretti per valori numerici */
.input-narrow {
max-width: 90px !important;
text-align: center;
padding: 0.2rem 0.4rem !important;
font-size: 0.8rem !important;
}
/* Responsive: su schermi piccoli, 2 colonne */
@media (max-width: 768px) {
.settings-grid-compact {
grid-template-columns: repeat(2, 1fr);
}
.input-narrow {
max-width: 100% !important;
}
}
.auction-log, .bidders-stats {
margin: 0.5rem;
margin: 0.25rem;
}
.auction-log h4, .bidders-stats h4 {
@@ -1227,56 +1293,56 @@ main {
margin-right: 0.5rem;
}
/* === PRODUCT INFO COMPATTO === */
.product-info-compact {
display: flex;
flex-direction: column;
gap: 1rem;
gap: 0.5rem;
}
/* Card info principali - orizzontali */
/* Card info principali - orizzontali compatte */
.info-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
gap: 0.4rem;
}
.info-card {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 6px;
gap: 0.5rem;
padding: 0.4rem 0.6rem;
border-radius: 4px;
border: 1px solid;
transition: all 0.2s ease;
transition: background-color 0.2s ease;
}
.info-card:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
background: var(--bg-hover);
}
.info-card i {
font-size: 1.75rem;
font-size: 1.1rem;
flex-shrink: 0;
}
.info-card div {
display: flex;
flex-direction: column;
gap: 0.125rem;
gap: 0;
}
.info-card small {
font-size: 0.688rem;
font-size: 0.6rem;
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 0.3px;
color: var(--text-muted);
font-weight: 500;
}
.info-card strong {
font-size: 1.125rem;
font-size: 0.9rem;
font-weight: 700;
color: var(--text-primary);
}
@@ -1299,26 +1365,26 @@ main {
color: var(--info-color);
}
/* Calcoli inline - 4 colonne */
/* Calcoli inline - 4 colonne compatte */
.calc-inline {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
padding: 0.75rem;
gap: 0.3rem;
padding: 0.4rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
border-radius: 4px;
}
.calc-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 0.5rem;
gap: 0.1rem;
padding: 0.25rem;
text-align: center;
border-radius: 4px;
transition: all 0.2s ease;
border-radius: 3px;
transition: background-color 0.2s ease;
}
.calc-item:hover {
@@ -1331,7 +1397,7 @@ main {
}
.calc-item i {
font-size: 1.25rem;
font-size: 0.9rem;
color: var(--primary-color);
}
@@ -1340,13 +1406,13 @@ main {
}
.calc-item .label {
font-size: 0.688rem;
font-size: 0.6rem;
color: var(--text-muted);
font-weight: 500;
}
.calc-item .value {
font-size: 1rem;
font-size: 0.85rem;
font-weight: 700;
color: var(--text-primary);
}
@@ -1355,30 +1421,30 @@ main {
.totals-compact {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: 0.75rem;
gap: 0.4rem;
align-items: center;
}
.total-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.75rem;
gap: 0.1rem;
padding: 0.4rem 0.6rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
border-radius: 4px;
}
.total-item span {
font-size: 0.75rem;
font-size: 0.65rem;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 0.375rem;
gap: 0.2rem;
}
.total-item strong {
font-size: 1.125rem;
font-size: 0.9rem;
font-weight: 700;
}
@@ -1398,10 +1464,10 @@ main {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-size: 1rem;
gap: 0.3rem;
padding: 0.4rem 0.8rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 700;
white-space: nowrap;
}
@@ -1419,7 +1485,7 @@ main {
}
.verdict-badge i {
font-size: 1.125rem;
font-size: 0.85rem;
}
/* === RESPONSIVE === */
@@ -1513,8 +1579,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 {

View File

@@ -1,5 +1,547 @@
/* === MODERN PAGE STYLES (append to app-wpf.css) === */
/* ═══════════════════════════════════════════════════════════════════
TOOLBAR COMPATTA CON PULSANTI E CONTEGGI
═══════════════════════════════════════════════════════════════════ */
.toolbar-compact {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
background: linear-gradient(135deg, rgba(20, 20, 30, 0.8) 0%, rgba(30, 30, 45, 0.8) 100%);
border-radius: var(--radius-md);
border: 1px solid rgba(255, 255, 255, 0.05);
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.btn-group-actions {
display: flex;
gap: 0.25rem;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: var(--radius-sm);
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
}
.action-btn.success {
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
color: white;
}
.action-btn.success:hover {
box-shadow: 0 0 10px rgba(34, 197, 94, 0.5);
transform: translateY(-1px);
}
.action-btn.warning {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: #1a1a1a;
}
.action-btn.warning:hover {
box-shadow: 0 0 10px rgba(245, 158, 11, 0.5);
transform: translateY(-1px);
}
.action-btn.secondary {
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
color: white;
}
.action-btn.secondary:hover {
box-shadow: 0 0 10px rgba(107, 114, 128, 0.5);
transform: translateY(-1px);
}
/* Indicatori Stato */
.status-indicators {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0 0.5rem;
border-left: 1px solid rgba(255, 255, 255, 0.1);
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.status-pill {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
transition: all 0.15s ease;
}
.status-pill:hover {
background: rgba(255, 255, 255, 0.1);
}
.status-pill i {
font-size: 0.7rem;
}
.status-pill.total { color: #a5b4fc; border-color: rgba(99, 102, 241, 0.3); }
.status-pill.active { color: #4ade80; border-color: rgba(34, 197, 94, 0.3); }
.status-pill.paused { color: #fbbf24; border-color: rgba(251, 191, 36, 0.3); }
.status-pill.stopped { color: #9ca3af; border-color: rgba(156, 163, 175, 0.3); }
.status-pill.won { color: #fbbf24; border-color: rgba(251, 191, 36, 0.3); }
.status-pill.lost { color: #f87171; border-color: rgba(248, 113, 113, 0.3); }
/* Pulsanti Gestione */
.btn-group-manage {
display: flex;
align-items: center;
gap: 0.25rem;
margin-left: auto;
}
.manage-separator {
width: 1px;
height: 20px;
background: rgba(255, 255, 255, 0.15);
margin: 0 0.25rem;
}
.manage-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 1px solid transparent;
border-radius: var(--radius-sm);
font-size: 0.8rem;
cursor: pointer;
transition: all 0.15s ease;
background: transparent;
}
.manage-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.manage-btn.primary {
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
color: white;
}
.manage-btn.primary:hover:not(:disabled) {
box-shadow: 0 0 8px rgba(99, 102, 241, 0.5);
}
.manage-btn.danger {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
}
.manage-btn.danger:hover:not(:disabled) {
box-shadow: 0 0 8px rgba(239, 68, 68, 0.5);
}
.manage-btn.danger-fill {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
color: white;
}
.manage-btn.danger-fill:hover:not(:disabled) {
box-shadow: 0 0 10px rgba(220, 38, 38, 0.6);
}
.manage-btn.outline-success {
border-color: rgba(34, 197, 94, 0.4);
color: #4ade80;
}
.manage-btn.outline-success:hover:not(:disabled) {
background: rgba(34, 197, 94, 0.15);
}
.manage-btn.outline-warning {
border-color: rgba(245, 158, 11, 0.4);
color: #fbbf24;
}
.manage-btn.outline-warning:hover:not(:disabled) {
background: rgba(245, 158, 11, 0.15);
}
.manage-btn.outline-secondary {
border-color: rgba(156, 163, 175, 0.4);
color: #9ca3af;
}
.manage-btn.outline-secondary:hover:not(:disabled) {
background: rgba(156, 163, 175, 0.15);
}
.manage-btn.outline-gold {
border-color: rgba(251, 191, 36, 0.4);
color: #fbbf24;
}
.manage-btn.outline-gold:hover:not(:disabled) {
background: rgba(251, 191, 36, 0.15);
}
.manage-btn.outline-danger {
border-color: rgba(239, 68, 68, 0.4);
color: #f87171;
}
.manage-btn.outline-danger:hover:not(:disabled) {
background: rgba(239, 68, 68, 0.15);
}
/* ═══════════════════════════════════════════════════════════════════
LOG BOX - SCROLL FISSO
═══════════════════════════════════════════════════════════════════ */
.log-box {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.75rem;
line-height: 1.4;
padding: 0.5rem;
height: 100%; /* Usa altezza dal pannello */
overflow-y: auto; /* Scroll verticale */
overflow-x: hidden;
}
.log-box-compact {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.75rem;
line-height: 1.4;
padding: 0.5rem;
max-height: 100%; /* Non superare il contenitore */
overflow-y: auto; /* Scroll verticale */
overflow-x: hidden;
}
.log-entry {
padding: 0.25rem 0.5rem;
margin-bottom: 0.15rem;
border-radius: 4px;
word-wrap: break-word;
transition: background 0.1s ease;
}
.log-entry:hover {
background: rgba(255, 255, 255, 0.05);
}
.log-entry-error {
color: #f87171;
background: rgba(239, 68, 68, 0.1);
border-left: 3px solid #ef4444;
padding-left: 0.5rem;
}
.log-entry-warning {
color: #fbbf24;
background: rgba(245, 158, 11, 0.1);
border-left: 3px solid #f59e0b;
padding-left: 0.5rem;
}
.log-entry-success {
color: #4ade80;
background: rgba(34, 197, 94, 0.1);
border-left: 3px solid #22c55e;
padding-left: 0.5rem;
}
.log-entry-debug {
color: #60a5fa;
opacity: 0.7;
}
/* ═══════════════════════════════════════════════════════════════════
LAYOUT CON SPLITTER TRASCINABILI
═══════════════════════════════════════════════════════════════════ */
/* Container principale - occupa tutto lo spazio disponibile */
.auction-monitor-container {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
overflow: hidden;
box-sizing: border-box;
gap: 0; /* IMPORTANTE: nessun gap tra toolbar e content */
}
/* Area contenuto principale */
.main-content-area {
display: flex;
flex-direction: column;
flex: 1 1 auto; /* Cresce e si riduce */
min-height: 0; /* IMPORTANTE per flex */
overflow: hidden;
gap: 0; /* IMPORTANTE: nessun gap, gli splitter gestiscono lo spazio */
}
/* Riga superiore (Aste + Log) */
.top-row {
display: flex;
flex-direction: row;
flex: 1 1 auto; /* Cresce e si riduce - NON percentuale fissa */
min-height: 200px; /* Altezza minima */
overflow: hidden;
gap: 0; /* IMPORTANTE: nessun gap, gutter gestisce lo spazio */
}
/* Riga inferiore (Dettagli) */
.bottom-row {
display: flex;
flex-direction: column;
flex: 0 0 auto; /* NON cresce automaticamente */
height: 300px; /* Altezza iniziale fissa */
min-height: 150px; /* Altezza minima */
overflow: hidden;
gap: 0;
}
/* Pannello generico */
.panel {
display: flex;
flex-direction: column;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
overflow: hidden;
box-sizing: border-box;
height: 100%; /* IMPORTANTE: altezza fissa dal contenitore */
position: relative; /* Per z-index */
}
/* IMPORTANTE: Previeni collasso dei bordi */
.panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: -1;
}
/* Pannello Aste */
.panel-auctions {
flex: 1 1 auto; /* Cresce per riempire lo spazio */
min-width: 300px;
height: 100%; /* Usa tutta l'altezza del contenitore */
overflow: hidden;
}
/* Pannello Log */
.panel-log {
flex: 0 0 auto; /* NON cresce/riduce automaticamente */
width: 320px; /* Larghezza fissa iniziale */
min-width: 200px;
max-width: 500px;
height: 100%; /* Usa tutta l'altezza del contenitore */
overflow: hidden;
}
/* Pannello Dettagli */
.panel-details {
flex: 1 1 auto; /* Cresce per riempire */
min-height: 150px;
height: 100%; /* Usa tutta l'altezza del contenitore */
overflow: hidden; /* Il contenitore non scrolla */
}
/* Gutter/Splitter */
.gutter {
background: rgba(255, 255, 255, 0.03);
flex-shrink: 0; /* NON si riduce mai */
flex-grow: 0; /* NON cresce mai */
transition: background 0.15s ease;
z-index: 10; /* Sopra i pannelli */
}
.gutter:hover {
background: rgba(99, 102, 241, 0.25);
}
.gutter:active {
background: rgba(99, 102, 241, 0.4);
}
.gutter-vertical {
width: 6px;
cursor: col-resize;
min-width: 6px; /* Larghezza fissa */
max-width: 6px;
}
.gutter-horizontal {
height: 6px;
cursor: row-resize;
min-height: 6px; /* Altezza fissa */
max-height: 6px;
}
/* Header pannello */
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.4rem 0.6rem;
background: rgba(255, 255, 255, 0.03);
border-bottom: 1px solid var(--border-color);
font-size: 0.75rem;
font-weight: 600;
color: var(--text-secondary);
flex-shrink: 0;
}
.panel-header i {
margin-right: 0.4rem;
opacity: 0.7;
}
.panel-content {
flex: 1;
overflow-y: auto; /* Scroll verticale */
overflow-x: hidden;
min-height: 0; /* Importante per flex */
max-height: 100%; /* Non superare il pannello */
}
/* Contenuto dettagli */
.auction-details-content {
padding: 0.5rem;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden; /* Il contenitore non scrolla */
}
/* Tab content deve scrollare */
.tab-content {
flex: 1;
overflow: hidden;
min-height: 0;
display: flex; /* IMPORTANTE per i tab-pane */
flex-direction: column;
}
.tab-pane {
height: 100%;
overflow: hidden;
display: none; /* Bootstrap lo gestisce con show active */
}
.tab-pane.show.active {
display: flex; /* Quando attivo diventa flex */
flex-direction: column;
}
.tab-panel-content {
flex: 1; /* Riempie il tab-pane */
overflow-y: auto; /* Scroll per il contenuto dei tab */
overflow-x: hidden;
padding: 0.5rem;
min-height: 0; /* IMPORTANTE per flex */
}
.details-header {
font-size: 0.9rem;
font-weight: 600;
padding: 0.25rem 0 0.5rem 0;
border-bottom: 1px solid var(--border-color);
margin-bottom: 0.5rem;
}
/* Placeholder dettagli */
.details-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 1rem;
color: var(--text-muted);
text-align: center;
}
.details-placeholder i {
font-size: 1.25rem;
margin-bottom: 0.375rem;
opacity: 0.5;
}
.details-placeholder p {
margin: 0;
font-size: 0.8rem;
}
/* Colonna Ping */
.col-ping {
width: 55px;
min-width: 55px;
text-align: center;
font-size: 0.7rem;
font-weight: 500;
}
/* Responsive */
@media (max-width: 992px) {
.top-row {
flex-direction: column;
}
.gutter-vertical {
display: none;
}
.panel-log {
flex: 0 0 150px;
max-width: none;
}
.toolbar-compact {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.btn-group-actions,
.status-indicators,
.btn-group-manage {
justify-content: center;
}
.status-indicators {
border: none;
padding: 0.5rem 0;
border-top: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.btn-group-manage {
margin-left: 0;
flex-wrap: wrap;
}
}
/* ✅ NUOVO: Stili per selezione riga e colonna puntate */
.table-hover tbody tr {
cursor: pointer;
@@ -28,6 +570,29 @@
white-space: nowrap;
}
/* 🔥 Header ordinabili */
.sortable-header {
cursor: pointer;
user-select: none;
transition: background-color 0.2s ease;
}
.sortable-header:hover {
background-color: rgba(13, 110, 253, 0.15);
}
/* 🎯 Evidenziazione riga utente corrente */
.my-bid-row {
background-color: rgba(40, 167, 69, 0.2) !important;
border-left: 3px solid #28a745;
font-weight: 500;
}
.my-bid-row:hover {
background-color: rgba(40, 167, 69, 0.3) !important;
}
.page-header {
display: flex;
align-items: center;
@@ -249,6 +814,7 @@
border: 1px solid var(--border-color) !important;
color: var(--text-secondary) !important;
border-radius: var(--radius-md) !important;
transition: filter 0.2s ease, background-color 0.2s ease;
}
.settings-container .btn-outline-secondary:hover {
@@ -256,6 +822,36 @@
color: var(--text-primary) !important;
}
/* 🎨 Stili hover moderni per pulsanti outline */
.btn-outline-primary,
.btn-outline-secondary,
.btn-outline-success,
.btn-outline-danger,
.btn-outline-warning,
.btn-outline-info {
transition: filter 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
}
.btn-outline-primary:hover:not(:disabled) {
filter: brightness(1.05);
box-shadow: 0 2px 6px rgba(13, 110, 253, 0.2);
}
.btn-outline-success:hover:not(:disabled) {
filter: brightness(1.05);
box-shadow: 0 2px 6px rgba(25, 135, 84, 0.2);
}
.btn-outline-danger:hover:not(:disabled) {
filter: brightness(1.05);
box-shadow: 0 2px 6px rgba(220, 53, 69, 0.2);
}
.btn-outline-warning:hover:not(:disabled) {
filter: brightness(1.05);
box-shadow: 0 2px 6px rgba(255, 193, 7, 0.2);
}
/* === AUCTION BROWSER STYLES === */
.browser-container {
@@ -658,3 +1254,52 @@
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%) !important;
color: #e5e5e5 !important;
}
/* ═══════════════════════════════════════════════════════════════════
BANNER STATISTICHE ASTE - RIMOSSO (sostituito da toolbar compatta)
═══════════════════════════════════════════════════════════════════ */
/* Legacy support - nascosto */
.auctions-stats-banner {
display: none;
}
/* ═══════════════════════════════════════════════════════════════════
TABELLA COMPATTA
═══════════════════════════════════════════════════════════════════ */
.table-compact {
font-size: 0.8rem;
}
.table-compact th {
padding: 0.4rem 0.5rem;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.3px;
white-space: nowrap;
}
.table-compact td {
padding: 0.35rem 0.5rem;
vertical-align: middle;
}
.table-compact .col-stato {
width: 80px;
}
.table-compact .col-azioni {
width: 90px;
}
/* Pulsanti extra small */
.btn-xs {
padding: 0.15rem 0.35rem;
font-size: 0.7rem;
line-height: 1.2;
}
.btn-xs i {
font-size: 0.75rem;
}

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;
}
}
};
})();