Compare commits

...

17 Commits

Author SHA1 Message Date
Alby96 e18a09e1da Gestione massiva limiti prodotto e ottimizzazione ticker
Aggiunta barra azioni per gestione massiva limiti prodotto in Statistics.razor (applica, salva, attiva/disattiva, copia consigliati). Uniformati simboli euro e messaggi in italiano. Ottimizzata la logica del ticker: controllo puntata ora avviene prima del polling, gestione fine asta differita tramite PendingEndState. Introdotto controllo esplicito su MaxClicks per asta. Implementata cache delle impostazioni in SettingsManager per ridurre accessi disco. Vari fix minori e miglioramenti di robustezza.
2026-03-03 08:53:38 +01:00
Alby96 f3262a0497 Log aste strutturato, limiti prodotto e UI statistiche
- Log per-asta ora strutturato con livelli, categorie e deduplicazione; motivi di blocco puntata tracciati in modo dettagliato e throttled
- Nuova visualizzazione log compatta e colorata nella UI
- Migliorate statistiche prodotto: aggiunta mediana prezzo, flag UseCustomLimits e editing limiti inline
- Impostazione priorità limiti nuove aste (globali vs personalizzati)
- Refactoring: rimossi limiti reset, UI statistiche rinnovata, ordinamenti e filtri avanzati
- Aggiornato schema DB (MedianFinalPrice, UseCustomLimits)
- Diagnostica periodica e log dettagliato su ticker/controlli
2026-02-16 23:10:04 +01:00
Alby96 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
Alby96 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
Alby96 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
Alby96 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
Alby96 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
Alby96 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
Alby96 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
Alby96 77eb9943d0 Gestione avanzata database e rimozione MaxClicks
Aggiunta sezione impostazioni per manutenzione database (auto-salvataggio, pulizia duplicati/incompleti, retention, ottimizzazione). Implementati metodi asincroni in DatabaseService per pulizia e statistiche. Pulizia automatica all’avvio secondo impostazioni. Rimossa la proprietà MaxClicks da modello, UI e logica. Migliorata la sicurezza thread-safe e la trasparenza nella gestione dati. Spostato il badge versione nelle info applicazione.
2026-01-24 01:30:49 +01:00
Alby96 a0ec72f6c0 Refactor: solo SQLite, limiti auto, UI statistiche nuova
Rimosso completamente il supporto a PostgreSQL: ora tutte le statistiche e i dati persistenti usano solo SQLite, con percorso configurabile tramite DATA_PATH per Docker/volumi. Aggiunta gestione avanzata delle statistiche per prodotto, limiti consigliati calcolati automaticamente e applicabili dalla UI. Rinnovata la pagina Statistiche con tabelle aste recenti e prodotti, rimosso il supporto a grafici legacy e a "Puntate Gratuite". Migliorata la ricerca e la gestione delle aste nel browser, aggiunta diagnostica avanzata e logging dettagliato per il database. Aggiornati Dockerfile e docker-compose: l'app è ora self-contained e pronta per l'uso senza database esterni.
2026-01-23 16:56:03 +01:00
Alby96 21a1d57cab Migliora badge stato aste: nuovi colori, icone, animazioni
Rivisti i metodi di calcolo e visualizzazione dello stato delle aste in Index.razor.cs, distinguendo tra stati di sistema e controllati dall’utente. Aggiunte nuove classi CSS e animazioni in modern-pages.css per badge più chiari, compatti e animati. Mantenuta compatibilità con classi Bootstrap legacy. Migliorata la leggibilità e l’usabilità della tabella aste.
2026-01-22 15:28:05 +01:00
Alby96 2833cd0487 Aggiornamento live aste, azioni rapide e scroll infinito
- Aggiornamento automatico degli stati delle aste ogni 500ms, rimosso il bottone manuale "Aggiorna Prezzi"
- Aggiunti pulsanti per copiare il link e aprire l'asta in nuova scheda
- Possibilità di rimuovere aste dal monitor direttamente dalla lista
- Caricamento aste ottimizzato: scroll infinito senza duplicati tramite nuova API get_auction_updates.php
- Migliorato il parsing dei dati e la precisione del countdown usando il timestamp del server
- Refactoring vari per migliorare la reattività e l'esperienza utente
2026-01-22 11:43:59 +01:00
Alby96 865bfa2752 Aggiunta pagina "Esplora Aste" per browser pubblico
Introdotta la funzionalità di esplorazione delle aste pubbliche di Bidoo senza login, accessibile dal menu principale.
Aggiunti nuovi modelli (`BidooBrowserAuction`, `BidooCategoryInfo`) e servizio (`BidooBrowserService`) per scraping e polling delle aste e categorie.
Creata la pagina Blazor `AuctionBrowser.razor` con griglia responsive, badge, filtri per categoria, caricamento incrementale e aggiornamento automatico degli stati.
Aggiornati i servizi in `Program.cs` e aggiunti nuovi stili CSS per la UI moderna.
Le aste possono essere aggiunte rapidamente al monitor personale. Parsing robusto e fallback su categorie predefinite in caso di errori.
2026-01-22 00:08:16 +01:00
Alby96 70ed8f0a61 Modernizzazione UI: nuovo tema dark e sidebar rivista
Aggiorna l’interfaccia Blazor con una palette dark moderna, font Inter, e una sidebar ridisegnata.
Riorganizza layout e navigazione, migliora la gestione errori e introduce nuovi stili per card, bottoni, input e badge.
Aggiunto `modern-pages.css` per header, griglie statistiche, alert e form più coerenti e attuali.
Migliora leggibilità, navigazione e user experience complessiva.
2026-01-21 17:39:15 +01:00
Alby96 ed42a41bcd Autenticazione Identity: login sicuro, lockout, UI aggiornata
- Integra ASP.NET Core Identity: login/password, lockout brute-force, cookie sicuri, password policy forte
- Seed automatico utente admin da variabili ambiente (fallback password temporanea forte)
- Tutte le pagine principali ora protette con [Authorize] e redirect automatico a /login
- Nuovo layout login/logout pulito senza sidebar, spinner durante redirect
- NavMenu mostra utente autenticato e logout
- Rimosse credenziali Bidoo da env/Docker: ora solo cookie sessione da UI
- Aggiornata documentazione: sicurezza, deploy, backup, troubleshooting
- Fix NavigationException, SectionRegistry, errori header read-only
- Versione incrementata a 1.2.0, pronto per deploy production Tailscale/Unraid
2026-01-21 17:00:51 +01:00
Alby96 6a3f931431 Fix definitivo porta 8080 + healthcheck e doc v1.1.2
Forzato UseUrls() su 8080 per evitare override e garantire che il container ascolti sempre sulla porta corretta. Migliorati i parametri del healthcheck Docker per Blazor Server (timeout 30s, start-period 90s, retries 5). Aggiornati metadati di versione a 1.1.2. Aggiunta documentazione dettagliata sul fix e corretti caratteri accentati nel changelog.
2026-01-21 12:42:34 +01:00
67 changed files with 409199 additions and 7150 deletions
+20 -30
View File
@@ -3,11 +3,21 @@
# === ASP.NET Core Configuration ===
ASPNETCORE_ENVIRONMENT=Production
ASPNETCORE_URLS=http://+:5000;https://+:5001
ASPNETCORE_URLS=http://+:8080
# === HTTPS Certificate ===
# Password per il certificato PFX
CERT_PASSWORD=AutoBidder2024
# === AUTENTICAZIONE APPLICAZIONE (SICUREZZA) ===
# Username amministratore
ADMIN_USERNAME=admin
# Password amministratore (OBBLIGATORIO in produzione!)
# REQUISITI: min 12 caratteri, maiuscole, minuscole, numeri, simboli
# Esempio: Admin@SecurePass2024!
ADMIN_PASSWORD=
# === NOTA: SESSIONE BIDOO ===
# Non servono credenziali Bidoo!
# Il cookie di sessione Bidoo viene configurato manualmente
# dall'interfaccia web in Settings ? Sessione Bidoo
# === PostgreSQL Database (Statistiche) ===
# Username PostgreSQL
@@ -20,34 +30,14 @@ POSTGRES_PASSWORD=autobidder_password
POSTGRES_DB=autobidder_stats
# Usa PostgreSQL per statistiche (true/false)
DATABASE_USE_POSTGRES=true
USE_POSTGRES=true
# Auto-crea schema PostgreSQL se mancante (true/false)
DATABASE_AUTO_CREATE_SCHEMA=true
# === Application Settings ===
# Logging level (Debug, Information, Warning, Error)
LOG_LEVEL=Information
# Fallback a SQLite se PostgreSQL non disponibile (true/false)
DATABASE_FALLBACK_TO_SQLITE=true
# === Gitea Container Registry ===
# URL del registry (senza https://)
GITEA_REGISTRY=192.168.30.23/Alby96
# Username Gitea
GITEA_USERNAME=Alby96
# Access Token Gitea (genera su: https://192.168.30.23/user/settings/applications)
# Scope richiesti: write:package, read:package
GITEA_PASSWORD=ghp_your_token_here
# === Deployment Configuration ===
# IP o hostname del server di deploy
DEPLOY_HOST=192.168.30.23
# User SSH per deploy
DEPLOY_USER=deploy
# Path alla chiave privata SSH (per CI/CD)
# DEPLOY_SSH_KEY_PATH=/path/to/ssh/key
# Porta applicazione (default: 8080 container, mappata su host)
APP_PORT=5000
# === Database Configuration ===
# Path database SQLite locale (default: /app/data/autobidder.db in container)
+36 -23
View File
@@ -1,23 +1,36 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Non trovato</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<div style="padding: 2rem; text-align: center;">
<svg style="width: 64px; height: 64px; margin-bottom: 1rem; opacity: 0.5;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<h1 style="font-size: 1.5rem; margin-bottom: 0.5rem;">Pagina non trovata</h1>
<p style="color: var(--text-muted);">Spiacenti, non c'e' nulla a questo indirizzo.</p>
<a href="/" style="color: var(--primary-color); text-decoration: none; margin-top: 1rem; display: inline-block;">
? Torna alla Home
</a>
</div>
</LayoutView>
</NotFound>
</Router>
<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 ad accedere a questa risorsa.</p>
}
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Non trovato</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<div style="padding: 2rem; text-align: center;">
<svg style="width: 64px; height: 64px; margin-bottom: 1rem; opacity: 0.5;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<h1 style="font-size: 1.5rem; margin-bottom: 0.5rem;">Pagina non trovata</h1>
<p style="color: var(--text-muted);">Spiacenti, non c'è nulla a questo indirizzo.</p>
<a href="/" style="color: var(--primary-color); text-decoration: none; margin-top: 1rem; display: inline-block;">
?? Torna alla Home
</a>
</div>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
+6 -5
View File
@@ -11,11 +11,11 @@
<DockerfileFile>Dockerfile</DockerfileFile>
<!-- Versioning per Docker & Gitea Registry -->
<!-- v1.1.0: Docker/Gitea publishing workflow + HTTPS fix -->
<Version>1.1.1</Version>
<AssemblyVersion>1.1.1.0</AssemblyVersion>
<FileVersion>1.1.1.0</FileVersion>
<InformationalVersion>1.1.1</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>
@@ -67,6 +67,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
-236
View File
@@ -1,236 +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.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.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.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
-
---
---
## Tipologie di Modifiche
- `? Aggiunte (Added)`: Nuove funzionalità
- `?? Modifiche (Changed)`: Modifiche a funzionalità esistenti
- `??? Rimossi (Removed)`: Funzionalità rimosse
- `?? Correzioni (Fixed)`: Bug fix
- `?? Sicurezza (Security)`: Fix di sicurezza
- `?? Deprecati (Deprecated)`: Funzionalità obsolete (da rimuovere)
## Versioning
Questo progetto segue [Semantic Versioning](https://semver.org/lang/it/):
- **MAJOR** (1.x.x ? 2.x.x): Breaking changes incompatibili
- **MINOR** (x.1.x ? x.2.x): Nuove feature retrocompatibili
- **PATCH** (x.x.1 ? x.x.2): Bug fix retrocompatibili
Esempi:
- `1.0.0` ? `1.1.0`: Nuova feature (Gitea publishing)
- `1.1.0` ? `1.1.1`: Bug fix
- `1.1.0` ? `2.0.0`: Breaking change (API cambiate)
-296
View File
@@ -1,296 +0,0 @@
# ?? CONFIGURAZIONE FINALE - UN SOLO PROFILO
## ? Cosa è Cambiato
### PRIMA (Configurazione Complessa)
- ? Due profili: `GiteaRegistry` e `GiteaRegistry-LocalOnly`
- ? Versionamento manuale
- ? Confusione su quale profilo usare
### DOPO (Configurazione Semplificata)
- ? **UN SOLO PROFILO**: `GiteaRegistry.pubxml`
- ? **Versionamento automatico** da `<Version>` della solution
- ? **Workflow chiaro** e lineare
---
## ?? Struttura Files
```
AutoBidder/
??? AutoBidder.csproj
? ??? <Version>1.0.0</Version> ? VERSIONE SOLUTION (fonte unica)
? ??? <Target Name="PushDockerImageToGitea"> ? Post-build automatico
??? Dockerfile ? Build immagine Docker
??? Properties/
? ??? PublishProfiles/
? ??? GiteaRegistry.pubxml ? UNICO PROFILO (tutto automatico)
??? DOCKER_PUBLISH_GUIDE.md ? Guida aggiornata
```
---
## ?? Come Funziona
### 1. Definisci Versione Solution
```xml
<!-- AutoBidder.csproj -->
<PropertyGroup>
<Version>1.0.1</Version> ? Modifica qui per nuova versione
</PropertyGroup>
```
### 2. Pubblica da Visual Studio
```
Tasto destro progetto ? Pubblica ? GiteaRegistry ? Pubblica
```
### 3. Sistema Automatico
```
???????????????????????????????????
? Visual Studio: Publish ?
???????????????????????????????????
?
?
???????????????????????????????????
? Build .NET (Release) ?
???????????????????????????????????
?
?
???????????????????????????????????
? Docker build ?
? ? autobidder:latest ?
???????????????????????????????????
?
?
???????????????????????????????????
? POST-BUILD (AutoBidder.csproj) ?
? ?
? Legge: <Version>1.0.1</Version> ?
? ?
? Tag: ?
? • autobidder:latest ?
? ? gitea.../alby96/ ?
? autobidder:latest ?
? ?
? • autobidder:latest ?
? ? gitea.../alby96/ ?
? autobidder:1.0.1 ?
???????????????????????????????????
?
?
???????????????????????????????????
? Push su Gitea ?
? ?
? ? latest (aggiornato) ?
? ? 1.0.1 (nuovo tag) ?
???????????????????????????????????
```
---
## ?? Versionamento Automatico
### Source of Truth
```xml
<!-- AutoBidder.csproj - UNICA FONTE DI VERITÀ -->
<Version>1.0.1</Version>
```
### Tag Generati Automaticamente
| Versione Solution | Tag Latest | Tag Versione | Nota |
|-------------------|------------|--------------|------|
| `1.0.0` | `:latest` ? 1.0.0 | `:1.0.0` | Prima versione |
| `1.0.1` | `:latest` ? 1.0.1 | `:1.0.1` + `:1.0.0` rimane | Latest aggiornato |
| `2.0.0` | `:latest` ? 2.0.0 | `:2.0.0` + precedenti | Major update |
### Storico Versioni su Gitea
Gitea mantiene **TUTTI i tag** pubblicati:
```
?? gitea.encke-hake.ts.net/alby96/autobidder
??? ??? latest (? 1.0.1) [sempre aggiornato]
??? ??? 1.0.1 [immutabile]
??? ??? 1.0.0 [immutabile]
??? ??? 0.9.0 [immutabile]
```
---
## ?? Esempio Pratico: Rilascio Versione 1.0.2
### Step 1: Aggiorna Versione
```xml
<!-- AutoBidder.csproj -->
<Version>1.0.2</Version> <!-- Era 1.0.1 -->
```
### Step 2: Pubblica
1. Tasto destro ? Pubblica
2. Seleziona `GiteaRegistry`
3. Click **Pubblica**
### Step 3: Output Automatico
```
?????????????????????????????????????????????????????????????????????
? POST-BUILD: Pubblicazione su Gitea Container Registry ?
?????????????????????????????????????????????????????????????????????
?? Solution Version: 1.0.2
??? Target Tags:
• gitea.encke-hake.ts.net/alby96/autobidder:latest
• gitea.encke-hake.ts.net/alby96/autobidder:1.0.2
? Tagged: gitea.../autobidder:latest
? Tagged: gitea.../autobidder:1.0.2
? Pushed: gitea.../autobidder:latest
? Pushed: gitea.../autobidder:1.0.2
?????????????????????????????????????????????????????????????????????
? ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ?
?????????????????????????????????????????????????????????????????????
?? Tag pubblicati:
• latest (ora punta a 1.0.2)
• 1.0.2 (nuova versione)
```
### Step 4: Verifica su Gitea
```
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder
```
Vedrai:
- `latest` ? digest aggiornato (ora è 1.0.2)
- `1.0.2` ? nuovo tag creato
- `1.0.1` ? ancora disponibile
- `1.0.0` ? ancora disponibile
---
## ?? Vantaggi del Nuovo Sistema
| Aspetto | Prima | Dopo |
|---------|-------|------|
| **Profili** | 2 (confusione) | 1 (chiaro) |
| **Versionamento** | Manuale | Automatico |
| **Source of Truth** | Multipli | Unico (`<Version>`) |
| **Complessità** | Alta | Bassa |
| **Errori** | Facili | Difficili |
| **Manutenibilità** | Difficile | Facile |
---
## ?? Best Practices
### 1. Semantic Versioning
Segui il formato: `MAJOR.MINOR.PATCH`
```xml
<!-- Esempi -->
<Version>1.0.0</Version> ? Release iniziale
<Version>1.0.1</Version> ? Bug fix
<Version>1.1.0</Version> ? Nuova feature
<Version>2.0.0</Version> ? Breaking change
```
### 2. Deploy Production
**? MAI usare `latest` in production:**
```yaml
# ERRATO
image: gitea.../autobidder:latest
```
**? USA sempre versione specifica:**
```yaml
# CORRETTO
image: gitea.../autobidder:1.0.2
```
### 3. Testing
Prima di deployare in production:
```bash
# 1. Pull versione specifica
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.0.2
# 2. Test locale
docker run -p 5000:8080 gitea.../autobidder:1.0.2
# 3. Verifica funzionalità
# http://localhost:5000
# 4. Se OK ? Deploy production
```
### 4. Changelog
Mantieni un file `CHANGELOG.md` nella repo:
```markdown
# Changelog
## [1.0.2] - 2026-01-18
### Fixed
- Correzione bug autenticazione Gitea
## [1.0.1] - 2026-01-17
### Added
- Supporto versionamento automatico
```
---
## ?? Comandi Rapidi
```bash
# Autenticazione (prima volta)
docker login gitea.encke-hake.ts.net
# Pubblica da Visual Studio
# Tasto destro ? Pubblica ? GiteaRegistry
# Pull versione specifica (production)
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.0.2
# Pull latest (development)
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
# Lista tutti i tag disponibili (via API)
curl https://gitea.encke-hake.ts.net/api/v1/packages/Alby96/container/autobidder
```
---
## ?? File Finali
| File | Scopo |
|------|-------|
| `AutoBidder.csproj` | Versione solution + post-build target |
| `Properties/PublishProfiles/GiteaRegistry.pubxml` | UNICO profilo pubblicazione |
| `Dockerfile` | Build immagine Docker |
| `.dockerignore` | Esclusioni Docker |
| `DOCKER_PUBLISH_GUIDE.md` | Guida utente completa |
| `VERIFICA_CONFIGURAZIONE_GITEA.md` | Checklist conformità |
| `NUOVO_WORKFLOW_RIEPILOGO.md` | Dettagli tecnici workflow |
| **`CONFIGURAZIONE_FINALE.md`** | **Questo documento** |
---
**? CONFIGURAZIONE COMPLETATA E SEMPLIFICATA!**
Ora hai un sistema **professionale**, **automatico** e **tracciabile** per gestire versioni Docker su Gitea! ??
-104
View File
@@ -1,104 +0,0 @@
# ?? AutoBidder - Docker Deploy su Gitea
Setup minimalista per build e deploy Docker.
---
## ?? Requisiti
- Docker Desktop running
- Login Gitea Registry:
```powershell
docker login gitea.encke-hake.ts.net
# Username: alby96
# Password: <personal-access-token>
```
**Genera token**: https://gitea.encke-hake.ts.net/user/settings/applications ? Permissions: `write:packages`
---
## ?? Publish da Visual Studio
```
Build ? Publish ? Docker ? Publish
```
**Automatico**:
- Build immagine Docker
- Tag: `latest`, `1.0.0`, `1.0.0-20260118`
- Push su Gitea Registry
**Registry**: https://gitea.encke-hake.ts.net/alby96/mimante/-/packages/container/autobidder
---
## ?? Aggiornare Versione
Modifica `AutoBidder.csproj`:
```xml
<PropertyGroup>
<Version>1.0.1</Version>
</PropertyGroup>
```
Poi publish come sopra.
---
## ?? Deploy Unraid
### Via Template
1. Unraid ? Docker ? Add Template
2. URL: `https://192.168.30.23/Alby96/Mimante/raw/branch/docker/deployment/unraid-template.xml`
3. Install "AutoBidder"
4. Configura:
- Port: `8888:8080`
- AppData: `/mnt/user/appdata/autobidder`
- PostgreSQL: `Host=192.168.30.23;Port=5432;...`
5. Apply
### Via Docker Compose
```bash
docker-compose up -d
```
Accesso: http://localhost:8080
---
## ?? Troubleshooting
### Publish fallisce: "unauthorized"
```powershell
docker login gitea.encke-hake.ts.net
# Retry publish
```
### Container non parte
```powershell
# Verifica porta libera
netstat -ano | findstr :8080
# Rebuild
docker build -t test .
```
---
## ?? File Configurazione
| File | Scopo |
|------|-------|
| `Dockerfile` | Build immagine multi-stage |
| `docker-compose.yml` | Deploy con PostgreSQL |
| `Properties/PublishProfiles/Docker.pubxml` | Profilo publish Visual Studio |
| `deployment/unraid-template.xml` | Template Unraid |
---
**Setup completo! Build ? Publish ? Docker per deployare! ??**
-505
View File
@@ -1,505 +0,0 @@
# Guida Pubblicazione Docker su Gitea Registry
Questa guida spiega come pubblicare l'immagine Docker di AutoBidder sul registry Gitea usando il **nuovo workflow integrato con Visual Studio**.
## Prerequisiti
1. **Docker installato e in esecuzione**
2. **Accesso al registry Gitea**: `gitea.encke-hake.ts.net`
3. **Token PAT** (Personal Access Token) con permessi `read:packages` e `write:packages`
## 1. Autenticazione con Gitea (OBBLIGATORIA)
Prima di pubblicare, devi autenticarti con il registry Gitea usando un **Token PAT**:
### Genera Token PAT
1. Vai su: `https://gitea.encke-hake.ts.net/user/settings/applications`
2. Click **Generate New Token**
3. Seleziona scope: **`read:packages`** + **`write:packages`**
4. Copia il token generato
### Autentica Docker
```bash
docker login gitea.encke-hake.ts.net
# Username: Alby96
# Password: [INCOLLA IL TOKEN PAT QUI]
```
**IMPORTANTE:** Se hai 2FA attivo su Gitea, il Token PAT è **OBBLIGATORIO** (la password normale non funziona).
---
# Guida Pubblicazione Docker su Gitea Registry
Questa guida spiega come pubblicare l'immagine Docker di AutoBidder sul registry Gitea con **versionamento automatico** basato sulla solution.
## Prerequisiti
1. **Docker Desktop** installato e in esecuzione
2. **Accesso al registry Gitea**: `gitea.encke-hake.ts.net`
3. **Token PAT** (Personal Access Token) con permessi `read:packages` e `write:packages`
---
## 1. Autenticazione con Gitea (OBBLIGATORIA - Una Volta)
### Genera Token PAT
1. Vai su: `https://gitea.encke-hake.ts.net/user/settings/applications`
2. Click **Generate New Token**
3. Nome: `Docker Registry Access`
4. Seleziona scope: **`read:packages`** + **`write:packages`**
5. Click **Generate Token**
6. **Copia il token** (non sarà più visibile!)
### Autentica Docker
```bash
docker login gitea.encke-hake.ts.net
# Username: Alby96
# Password: [INCOLLA IL TOKEN PAT]
```
**? Success:** `Login Succeeded`
**?? IMPORTANTE:** Con 2FA attivo su Gitea, il Token PAT è **OBBLIGATORIO** (la password normale non funziona).
---
## 2. Pubblicare su Gitea con Versionamento Automatico
### ?? Workflow Completo in 3 Step
#### Step 1: Aggiorna Versione Solution (Opzionale)
Apri `AutoBidder.csproj` e modifica:
```xml
<Version>1.0.1</Version> <!-- Incrementa la versione -->
```
La versione qui definita sarà usata per taggare l'immagine Docker.
#### Step 2: Pubblica da Visual Studio
1. **Tasto destro** sul progetto `AutoBidder`
2. Seleziona **Pubblica**
3. Scegli il profilo: **`GiteaRegistry`** (UNICO profilo disponibile)
4. Click **Pubblica**
#### Step 3: Verifica Pubblicazione
Il sistema mostrerà output dettagliato:
```
?????????????????????????????????????????????????????????????????????
? POST-BUILD: Pubblicazione su Gitea Container Registry ?
?????????????????????????????????????????????????????????????????????
?? Solution Version: 1.0.1
?? Local Image: autobidder:latest
??? Target Tags:
• gitea.encke-hake.ts.net/alby96/autobidder:latest
• gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
???????????????????????????????????????????????????????????????????
??? Tagging images...
???????????????????????????????????????????????????????????????????
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:latest
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
???????????????????????????????????????????????????????????????????
?? Pushing to Gitea Registry...
???????????????????????????????????????????????????????????????????
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:latest
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
?????????????????????????????????????????????????????????????????????
? ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ?
?????????????????????????????????????????????????????????????????????
```
---
## 3. Sistema di Versionamento
### Come Funziona
Il versionamento è **completamente automatico** e basato su:
```xml
<!-- AutoBidder.csproj -->
<Version>1.0.1</Version>
```
Quando pubblichi:
- ? Tag `latest` ? **sempre aggiornato** all'ultima versione
- ? Tag `1.0.1` ? **versione specifica** immutabile
### Esempi Pratici
**Scenario 1: Prima pubblicazione**
```xml
<Version>1.0.0</Version>
```
Risultato:
- `gitea.../alby96/autobidder:latest` ? v1.0.0
- `gitea.../alby96/autobidder:1.0.0` ? v1.0.0
**Scenario 2: Aggiornamento versione**
```xml
<Version>1.0.1</Version>
```
Risultato:
- `gitea.../alby96/autobidder:latest` ? **aggiornato** a v1.0.1
- `gitea.../alby96/autobidder:1.0.1` ? **nuovo tag** creato
- `gitea.../alby96/autobidder:1.0.0` ? rimane disponibile
### Best Practices
| Ambiente | Tag Consigliato | Motivo |
|----------|----------------|---------|
| **Development** | `latest` | Sempre l'ultima versione |
| **Staging** | `1.0.1` | Versione specifica per test |
| **Production** | `1.0.1` | Versione immutabile e tracciabile |
---
## 4. Dove Trovare le Immagini Pubblicate
### Link Diretto al Package
```
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder
```
### Lista Packages Utente
```
https://gitea.encke-hake.ts.net/Alby96/-/packages
```
Su Gitea vedrai:
- ?? Nome: **`autobidder`**
- ??? Tag: `latest`, `1.0.0`, `1.0.1`, ...
- ?? Data pubblicazione
- ?? Digest SHA256
- ?? Dimensione immagine
---
## 5. Usare l'Immagine Pubblicata
### Pull con Versione Specifica
```bash
# Versione immutabile (CONSIGLIATO per production)
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
# Latest (sempre aggiornato)
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
```
### Su Unraid
1. Docker tab ? **Add Container**
2. **Repository**: `gitea.encke-hake.ts.net/alby96/autobidder:1.0.1`
3. **Port**: `5000` ? `8080`
4. **Volume 1**: `/mnt/user/appdata/autobidder/data` ? `/app/Data`
5. **Volume 2**: `/mnt/user/appdata/autobidder/logs` ? `/app/logs`
6. **Environment**: `ASPNETCORE_ENVIRONMENT=Production`
7. **Restart**: `unless-stopped`
### Docker Compose
```yaml
version: '3.8'
services:
autobidder:
image: gitea.encke-hake.ts.net/alby96/autobidder:1.0.1 # Versione specifica
container_name: autobidder
ports:
- "5000:8080"
volumes:
- ./data:/app/Data
- ./logs:/app/logs
environment:
- ASPNETCORE_ENVIRONMENT=Production
restart: unless-stopped
```
### Docker Run
```bash
docker run -d \
--name autobidder \
-p 5000:8080 \
-v /path/to/data:/app/Data \
-v /path/to/logs:/app/logs \
-e ASPNETCORE_ENVIRONMENT=Production \
--restart unless-stopped \
gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
```
---
## 6. Troubleshooting
### Errore: "unauthorized: authentication required"
```bash
# Re-autentica con Token PAT
docker logout gitea.encke-hake.ts.net
docker login gitea.encke-hake.ts.net
# Username: Alby96
# Password: [TOKEN PAT]
```
### Errore: "denied: requested access to the resource is denied"
**Causa:** Token PAT senza permessi corretti o scaduto
**Soluzione:**
1. Vai su: `https://gitea.encke-hake.ts.net/user/settings/applications`
2. Verifica che il token abbia: `read:packages` + `write:packages`
3. Se scaduto, genera nuovo token
### Container non parte: errore certificato HTTPS
**Sintomo:**
```
System.InvalidOperationException: Unable to configure HTTPS endpoint.
No server certificate was specified, and the default developer certificate
could not be found or is out of date.
```
**Causa:** Kestrel cerca di abilitare HTTPS ma non trova certificati di sviluppo nel container.
**? RISOLTO:**
- HTTPS disabilitato di default in container (`Kestrel__EnableHttps=false`)
- Porta HTTP: `8080` (standard container)
- SSL gestito dal reverse proxy (nginx/traefik) in production
**Per abilitare HTTPS manualmente** (se hai un certificato):
```bash
docker run -d \
-e Kestrel__EnableHttps=true \
-e Kestrel__Certificates__Default__Path=/path/to/cert.pfx \
-e Kestrel__Certificates__Default__Password=yourpassword \
-v /path/to/certs:/certs \
gitea.../autobidder:latest
```
### Errore: "La compilazione non è riuscita" ma il push è riuscito
**Sintomo:**
Visual Studio mostra:
```
Errore MSB4057: la destinazione "ContainerBuild" non è presente nel progetto
```
Ma nel log vedi:
```
? Pushed: gitea.../autobidder:latest
? Pushed: gitea.../autobidder:1.0.0
```
**Causa:** Il profilo stava usando `WebPublishMethod=Docker` che richiede Microsoft.Docker.Sdk non installato.
**? RISOLTO:** Il profilo è stato corretto per usare `WebPublishMethod=Custom` che non richiede SDK aggiuntivi.
### Verifica push su Gitea
```bash
# Test manuale push
docker push gitea.encke-hake.ts.net/alby96/autobidder:latest
# Se fallisce, verifica autenticazione
docker logout gitea.encke-hake.ts.net
docker login gitea.encke-hake.ts.net
```
### Versione non cambia su Gitea
**Verifica:**
1. Hai modificato `<Version>` in `AutoBidder.csproj`?
2. Hai fatto Rebuild completo?
3. Visual Studio ha mostrato il nuovo numero versione nell'output?
**Soluzione:** Rebuild completo
```bash
# Da Visual Studio: Build ? Rebuild Solution
# Poi: Tasto destro ? Pubblica ? GiteaRegistry
```
---
## 7. Riferimenti
- **Registry URL**: `https://gitea.encke-hake.ts.net`
- **Repository Codice**: `https://gitea.encke-hake.ts.net/Alby96/Mimante`
- **Packages Container**: `https://gitea.encke-hake.ts.net/Alby96/-/packages`
- **Package Autobidder**: `https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder`
- **Convenzione Gitea**: `{registro}/{owner}/{image}:{tag}` (3 livelli)
---
## 8. Riepilogo Comandi Rapidi
```bash
# 1. Autenticazione (prima volta)
docker login gitea.encke-hake.ts.net
# 2. Pubblica da Visual Studio
# Tasto destro ? Pubblica ? GiteaRegistry
# 3. Pull versione specifica
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
# 4. Pull latest
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
# 5. Run container
docker run -d --name autobidder \
-p 5000:8080 \
-v /data:/app/Data \
gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
```
---
**? CONFIGURAZIONE COMPLETATA!**
Ora hai un sistema di pubblicazione Docker con **versionamento automatico** completamente integrato! ??
## 3. Dove Trovare il Package su Gitea
**IL PACKAGE E' PUBBLICATO!** Cercalo in uno di questi percorsi:
### Percorso 1: Packages del Tuo Profilo (PRINCIPALE)
```
https://gitea.encke-hake.ts.net/Alby96/-/packages
```
Cerca un package di tipo **Container** con nome: `mimante/autobidder` oppure `mimante`
### Percorso 2: Explore Packages
```
https://gitea.encke-hake.ts.net/explore/packages
```
Filtra per tipo "Container" e cerca `mimante` o `autobidder`
### Percorso 3: Packages del Repository
```
https://gitea.encke-hake.ts.net/Alby96/Mimante/-/packages
```
### Verifica Push Riuscito
Se hai eseguito il push e vedi nell'output:
```
latest: digest: sha256:cb7621ed1f22... size: 856
```
Significa che **il package E' STATO pubblicato correttamente!**
Per verificare:
```bash
docker push gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
```
## 4. Usare l'Immagine Pubblicata
### Su Unraid
1. Vai su **Docker** tab
2. Click **Add Container**
3. **Repository**: `gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest`
4. **Port**: `5000` -> `8080` (container)
5. **Volume**: `/mnt/user/appdata/autobidder/data` -> `/app/Data`
6. **Volume**: `/mnt/user/appdata/autobidder/logs` -> `/app/logs`
7. **Environment**: `ASPNETCORE_ENVIRONMENT=Production`
### Docker Compose
```yaml
version: '3.8'
services:
autobidder:
image: gitea.encke-hake.ts.net/alby96/autobidder:latest
container_name: autobidder
ports:
- "5000:8080"
volumes:
- ./data:/app/Data
- ./logs:/app/logs
environment:
- ASPNETCORE_ENVIRONMENT=Production
restart: unless-stopped
```
### Docker Run
```bash
docker pull gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
docker run -d --name autobidder -p 5000:8080 -v /path/to/data:/app/Data -v /path/to/logs:/app/logs -e ASPNETCORE_ENVIRONMENT=Production gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
```
## 5. Aggiornare la Versione
1. Apri `AutoBidder.csproj`
2. Modifica il tag `<Version>`:
```xml
<Version>1.0.1</Version>
```
3. Pubblica:
```bash
dotnet publish /p:PublishProfile=GiteaRegistry
```
## Troubleshooting
### Errore: "unauthorized: authentication required"
```bash
docker login gitea.encke-hake.ts.net
```
### Package non visibile su Gitea
**Il package c'e'!** Controlla in:
- `https://gitea.encke-hake.ts.net/Alby96/-/packages` (packages utente)
- `https://gitea.encke-hake.ts.net/explore/packages` (tutti)
Cerca per nome: `mimante`, `autobidder`, o `mimante/autobidder` (tipo: Container)
Se vedi `digest: sha256:...` nel push, il package E' pubblicato.
## Riferimenti
- **Registry**: `https://gitea.encke-hake.ts.net`
- **Repository**: `https://gitea.encke-hake.ts.net/Alby96/Mimante`
- **Packages**: `https://gitea.encke-hake.ts.net/Alby96/-/packages` ?
- **Package Diretto**: `https://gitea.encke-hake.ts.net/Alby96/-/packages/container/mimante%2Fautobidder/latest`
- **Immagine**: `gitea.encke-hake.ts.net/alby96/mimante/autobidder`
## Comandi Rapidi
```bash
# 1. Login
docker login gitea.encke-hake.ts.net
# 2. Build e push
dotnet publish /p:PublishProfile=GiteaRegistry
# 3. Pull
docker pull gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
# 4. Run
docker run -d --name autobidder -p 5000:8080 -v /data:/app/Data gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
```
---
**Il package E' stato pubblicato!** Verifica su: `https://gitea.encke-hake.ts.net/Alby96/-/packages`
+27
View File
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using AutoBidder.Models;
namespace AutoBidder.Data;
/// <summary>
/// DbContext per autenticazione Identity
/// </summary>
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Personalizza nomi tabelle Identity (opzionale)
builder.Entity<ApplicationUser>(entity =>
{
entity.ToTable("Users");
});
}
}
+11 -2
View File
@@ -56,14 +56,23 @@ ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production
ENV Kestrel__EnableHttps=false
# Database path - tutti i database SQLite e dati persistenti
# Può essere sovrascritto nel docker-compose per mappare un volume persistente
ENV DATA_PATH=/app/Data
# Autenticazione applicazione (OBBLIGATORIO)
ENV ADMIN_USERNAME=admin
ENV ADMIN_PASSWORD=
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
# Aumentato timeout e start-period per Blazor Server
HEALTHCHECK --interval=30s --timeout=30s --start-period=90s --retries=5 \
CMD curl -f http://localhost:8080/ || exit 1
# Labels for metadata
LABEL org.opencontainers.image.title="AutoBidder" \
org.opencontainers.image.description="Sistema automatizzato gestione aste Bidoo - Blazor .NET 8" \
org.opencontainers.image.version="1.1.1" \
org.opencontainers.image.version="1.2.0" \
org.opencontainers.image.vendor="Alby96" \
org.opencontainers.image.source="https://gitea.encke-hake.ts.net/Alby96/Mimante"
@@ -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`
@@ -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`
+26
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
-363
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! ????**
@@ -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! ???**
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-304
View File
@@ -1,304 +0,0 @@
# ?? Fix: Container in ascolto su porta sbagliata
## ? Problema
**Sintomo:**
- Container si avvia senza errori
- Log mostra: `Now listening on: http://[::]:5000`
- Pagina non carica quando accedi a `http://localhost:5000`
- Port mapping: `5000:8080` (host:container)
**Causa:**
La configurazione esplicita di Kestrel nel `Program.cs` veniva sovrascritta da configurazioni di default, facendo ascoltare il server sulla porta 5000 invece che 8080.
---
## ?? Diagnosi
### Log Container
```
warn: Microsoft.AspNetCore.Server.Kestrel[0]
Overriding address(es) 'http://+:8080'. Binding to endpoints defined via IConfiguration and/or UseKestrel() instead.
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://[::]:5000 ? PROBLEMA QUI!
```
### Configurazione Attesa
```dockerfile
# Dockerfile
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
```
```yaml
# docker-compose.yml
ports:
- "5000:8080" # Host 5000 ? Container 8080
```
### Configurazione Effettiva
```
Container ascolta su: 5000 ?
Port mapping cerca: 8080 ?
Risultato: MISMATCH!
```
---
## ? Soluzione Applicata
### Prima (PROBLEMA)
```csharp
// Program.cs
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(8080); // ? Ignorato da Kestrel!
// ...
});
```
**Problema:** La configurazione esplicita viene sovrascritta dalle impostazioni di default di Kestrel.
### Dopo (RISOLTO)
```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// NON configurare esplicitamente HTTP (usa ASPNETCORE_URLS)
// Configura solo HTTPS se richiesto
var enableHttps = builder.Configuration.GetValue<bool>("Kestrel:EnableHttps", false);
if (enableHttps)
{
builder.WebHost.ConfigureKestrel(options =>
{
// Solo configurazione HTTPS (porta 8443)
// HTTP gestito da ASPNETCORE_URLS automaticamente
});
}
else
{
// Nessuna configurazione Kestrel
// ASPNETCORE_URLS=http://+:8080 gestisce tutto
Console.WriteLine($"[Kestrel] Listening on: {ASPNETCORE_URLS}");
}
```
**Benefici:**
- ? `ASPNETCORE_URLS` controlla la porta HTTP
- ? Configurazione centralizzata nel Dockerfile
- ? Facile override con variabili ambiente
- ? Meno conflitti tra configurazioni
---
## ?? Come Funziona Ora
### Precedenza Configurazione Kestrel
1. **ASPNETCORE_URLS** (da Dockerfile/env)
2. Configurazione IConfiguration
3. ~~UseKestrel() esplicito~~ (rimosso per HTTP)
### Flusso Startup
```
1. Dockerfile ? ENV ASPNETCORE_URLS=http://+:8080
2. Container start
3. Program.cs ? NO configurazione esplicita HTTP
4. Kestrel legge ASPNETCORE_URLS
5. ? Ascolta su porta 8080
```
### Log Atteso
```
[Kestrel] HTTPS disabled - running in HTTP-only mode
[Kestrel] Use a reverse proxy for SSL termination
[Kestrel] Listening on: http://+:8080
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://[::]:8080 ? CORRETTO!
```
---
## ?? Test della Correzione
### 1. Rebuild Container
```bash
# Build nuova immagine
docker build -t autobidder:latest .
# Verifica listening port nei log
docker run --rm autobidder:latest
# Output atteso:
# Now listening on: http://[::]:8080 ?
```
### 2. Test con docker-compose
```bash
docker-compose down
docker-compose build
docker-compose up -d
# Verifica log
docker-compose logs -f autobidder
# Accedi a http://localhost:5000
# (host porta 5000 ? container porta 8080)
```
### 3. Test Manuale
```bash
# Run container
docker run -d \
--name test-autobidder \
-p 5000:8080 \
autobidder:latest
# Verifica porta
docker port test-autobidder
# Output: 8080/tcp -> 0.0.0.0:5000 ?
# Test endpoint
curl http://localhost:5000
# Dovrebbe rispondere ?
# Cleanup
docker stop test-autobidder
docker rm test-autobidder
```
---
## ?? Port Mapping Corretto
### Docker Run
```bash
# Corretto: Host 5000 ? Container 8080
docker run -p 5000:8080 autobidder:latest
# Alternativa: Qualsiasi porta host
docker run -p 3000:8080 autobidder:latest # http://localhost:3000
docker run -p 8080:8080 autobidder:latest # http://localhost:8080
```
### Docker Compose
```yaml
services:
autobidder:
ports:
- "5000:8080" # Host:Container ?
environment:
- ASPNETCORE_URLS=http://+:8080 # Conferma porta container
```
### Unraid
```
Container Port: 8080
Host Port: 5000 (o qualsiasi altra porta disponibile)
```
---
## ?? Override Porta Container
Se vuoi cambiare la porta del container:
```bash
# Opzione 1: Environment variable
docker run -p 5000:9000 \
-e ASPNETCORE_URLS=http://+:9000 \
autobidder:latest
# Opzione 2: Modifica Dockerfile
# ENV ASPNETCORE_URLS=http://+:9000
# EXPOSE 9000
```
---
## ?? Troubleshooting
### Problema: Pagina ancora non carica
**Verifica porta container:**
```bash
docker ps
# PORTS: 0.0.0.0:5000->8080/tcp ?
# Verifica listening port dentro container
docker exec <container-id> netstat -tuln | grep LISTEN
# tcp6 0 0 :::8080 :::* LISTEN ?
```
**Verifica firewall:**
```bash
# Windows: Disabilita temporaneamente firewall
# Linux:
sudo ufw allow 5000/tcp
```
**Verifica log applicazione:**
```bash
docker logs <container-id>
# Cerca errori dopo "Application started"
```
### Problema: Port already in use
```bash
# Trova processo su porta 5000
# Windows:
netstat -ano | findstr :5000
taskkill /PID <PID> /F
# Linux:
lsof -i :5000
kill <PID>
```
---
## ? Checklist Fix Applicato
- [x] Rimossa configurazione esplicita HTTP in `Program.cs`
- [x] `ASPNETCORE_URLS` gestisce porta HTTP
- [x] Configurazione Kestrel solo per HTTPS opzionale
- [x] Log mostra porta corretta (8080)
- [x] Container accessibile da host
- [x] Build compila senza errori
- [x] Documentazione aggiornata
---
## ?? Lezioni Apprese
1. **ASPNETCORE_URLS ha precedenza limitata**
- Configurazione esplicita Kestrel sovrascrive ASPNETCORE_URLS
- Meglio non configurare esplicitamente se usi variabili ambiente
2. **Separare HTTP da HTTPS**
- HTTP: gestito da ASPNETCORE_URLS
- HTTPS: configurato esplicitamente (se necessario)
3. **Verifica sempre i log**
- "Now listening on:" mostra la porta effettiva
- Ignora warning su port override se tutto funziona
4. **Port mapping deve corrispondere**
- Container port = porta in "Now listening on:"
- Host port = quello che usi nel browser
---
**? FIX APPLICATO - Container ora ascolta correttamente sulla porta 8080!**
+29
View File
@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Identity;
namespace AutoBidder.Models;
/// <summary>
/// Utente dell'applicazione con supporto Identity
/// </summary>
public class ApplicationUser : IdentityUser
{
/// <summary>
/// Data creazione utente
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// Data ultimo accesso
/// </summary>
public DateTime? LastLoginAt { get; set; }
/// <summary>
/// Indica se l'utente è attivo
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// Note amministrative sull'utente
/// </summary>
public string? Notes { get; set; }
}
+450 -14
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
@@ -37,8 +38,14 @@ namespace AutoBidder.Models
public double MaxPrice { get; set; } = 0;
public int MinResets { get; set; } = 0; // Numero minimo reset prima di puntare
public int MaxResets { get; set; } = 0; // Numero massimo reset (0 = illimitati)
/// <summary>
/// Numero massimo di puntate consentite per questa asta (0 = illimitato).
/// Impostato dall'utente nella griglia statistiche o dai limiti prodotto.
/// Controllato in ShouldBid contro BidsUsedOnThisAuction.
/// </summary>
[JsonPropertyName("MaxClicks")]
public int MaxClicks { get; set; } = 0; // Numero massimo di puntate consentite (0 = illimitato)
public int MaxClicks { get; set; } = 0;
// Stato asta
public bool IsActive { get; set; } = true;
@@ -59,10 +66,54 @@ 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; }
/// <summary>
/// Stato di fine asta ricevuto dal poll ma non ancora processato.
/// Il ticker ha un'ultima occasione di puntare prima che venga gestito.
/// </summary>
[JsonIgnore]
public AuctionState? PendingEndState { get; set; }
// Storico
public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>();
public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase);
@@ -74,9 +125,9 @@ namespace AutoBidder.Models
[JsonPropertyName("RecentBids")]
public List<BidHistoryEntry> RecentBids { get; set; } = new List<BidHistoryEntry>();
// Log per-asta (non serializzato)
// Log per-asta strutturato (non serializzato)
[System.Text.Json.Serialization.JsonIgnore]
public List<string> AuctionLog { get; set; } = new();
public List<AuctionLogEntry> AuctionLog { get; set; } = new();
// Flag runtime: indica che è in corso un'operazione di final attack per questa asta
[System.Text.Json.Serialization.JsonIgnore]
@@ -122,26 +173,411 @@ 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 strutturata al log dell'asta con deduplicazione e limite righe.
/// Parsifica automaticamente il tag [TAG] per determinare livello e categoria.
/// </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)
public void AddLog(string message, int maxLines = 200)
{
var entry = $"{DateTime.Now:HH:mm:ss.fff} - {message}";
AuctionLog.Add(entry);
// Protezione null-safety (dopo ClearData)
if (AuctionLog == null) AuctionLog = new();
var now = DateTime.Now;
// Parsifica tag dal messaggio per determinare livello e categoria
var (level, category, cleanMessage) = ParseLogTag(message);
// DEDUPLICAZIONE: Se l'ultimo messaggio è uguale, incrementa contatore
if (AuctionLog.Count > 0)
{
var last = AuctionLog[^1];
if (last.Message == cleanMessage && last.Category == category)
{
last.RepeatCount++;
last.Timestamp = now;
return;
}
}
AuctionLog.Add(new AuctionLogEntry
{
Timestamp = now,
Level = level,
Category = category,
Message = cleanMessage
});
// 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);
AuctionLog.RemoveRange(0, AuctionLog.Count - maxLines);
}
}
/// <summary>
/// Aggiunge una voce tipizzata al log dell'asta (senza parsing del tag).
/// </summary>
public void AddLog(string message, AuctionLogLevel level, AuctionLogCategory category)
{
// Protezione null-safety (dopo ClearData)
if (AuctionLog == null) AuctionLog = new();
var now = DateTime.Now;
if (AuctionLog.Count > 0)
{
var last = AuctionLog[^1];
if (last.Message == message && last.Category == category)
{
last.RepeatCount++;
last.Timestamp = now;
return;
}
}
AuctionLog.Add(new AuctionLogEntry
{
Timestamp = now,
Level = level,
Category = category,
Message = message
});
if (AuctionLog.Count > MAX_LOG_LINES)
{
AuctionLog.RemoveRange(0, AuctionLog.Count - MAX_LOG_LINES);
}
}
/// <summary>
/// Parsifica i tag [TAG] per determinare livello e categoria automaticamente.
/// </summary>
private static (AuctionLogLevel level, AuctionLogCategory category, string cleanMessage) ParseLogTag(string message)
{
// Cerca pattern [TAG] all'inizio del messaggio
var tagMatch = System.Text.RegularExpressions.Regex.Match(message, @"^\[([A-Z_ ]+)\]\s*(.*)$");
if (!tagMatch.Success)
return (AuctionLogLevel.Info, AuctionLogCategory.General, message);
var tag = tagMatch.Groups[1].Value.Trim();
var cleanMsg = tagMatch.Groups[2].Value;
return tag switch
{
// Bid/puntata
"BID" => (AuctionLogLevel.Bid, AuctionLogCategory.BidAttempt, cleanMsg),
"BID OK" => (AuctionLogLevel.Success, AuctionLogCategory.BidResult, cleanMsg),
"BID FAIL" => (AuctionLogLevel.Error, AuctionLogCategory.BidResult, cleanMsg),
"BID EXCEPTION" => (AuctionLogLevel.Error, AuctionLogCategory.BidResult, cleanMsg),
"MANUAL BID" => (AuctionLogLevel.Bid, AuctionLogCategory.BidAttempt, cleanMsg),
"MANUAL BID OK" => (AuctionLogLevel.Success, AuctionLogCategory.BidResult, cleanMsg),
"MANUAL BID FAIL" => (AuctionLogLevel.Error, AuctionLogCategory.BidResult, cleanMsg),
// Timing
"TICKER" => (AuctionLogLevel.Timing, AuctionLogCategory.Ticker, cleanMsg),
"TIMING" or "\u26a0\ufe0f TIMING" => (AuctionLogLevel.Warning, AuctionLogCategory.Ticker, cleanMsg),
// Prezzi/limiti
"PRICE" => (AuctionLogLevel.Warning, AuctionLogCategory.Price, cleanMsg),
"VALUE" => (AuctionLogLevel.Warning, AuctionLogCategory.Value, cleanMsg),
"LIMIT" => (AuctionLogLevel.Warning, AuctionLogCategory.Limit, cleanMsg),
// Reset
var r when r.StartsWith("RESET") => (AuctionLogLevel.Info, AuctionLogCategory.Reset, cleanMsg),
// Strategie
"STRATEGY" => (AuctionLogLevel.Strategy, AuctionLogCategory.Strategy, cleanMsg),
"COMPETITION" => (AuctionLogLevel.Strategy, AuctionLogCategory.Competition, cleanMsg),
// Diagnostica
"DIAG" => (AuctionLogLevel.Debug, AuctionLogCategory.Diagnostic, cleanMsg),
"DEBUG" => (AuctionLogLevel.Debug, AuctionLogCategory.General, cleanMsg),
// Stato
"START" => (AuctionLogLevel.Info, AuctionLogCategory.Status, cleanMsg),
"ASTA TERMINATA" => (AuctionLogLevel.Warning, AuctionLogCategory.Status, cleanMsg),
"\u26a0\ufe0f SUGGERIMENTO" => (AuctionLogLevel.Warning, AuctionLogCategory.Ticker, cleanMsg),
// Polling
"POLL ERROR" => (AuctionLogLevel.Error, AuctionLogCategory.Polling, cleanMsg),
// Errori generici
"ERROR" or "ERRORE" => (AuctionLogLevel.Error, AuctionLogCategory.General, cleanMsg),
"WARN" => (AuctionLogLevel.Warning, AuctionLogCategory.General, cleanMsg),
"OK" => (AuctionLogLevel.Success, AuctionLogCategory.General, cleanMsg),
_ => (AuctionLogLevel.Info, AuctionLogCategory.General, message)
};
}
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;
PendingEndState = 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>
+126
View File
@@ -0,0 +1,126 @@
using System;
namespace AutoBidder.Models
{
/// <summary>
/// Entry strutturata per il log di una singola asta.
/// Contiene timestamp preciso, livello di gravità, categoria e messaggio.
/// </summary>
public class AuctionLogEntry
{
public DateTime Timestamp { get; set; }
public AuctionLogLevel Level { get; set; }
public AuctionLogCategory Category { get; set; }
public string Message { get; set; } = "";
/// <summary>
/// Contatore deduplicazione (se > 1, il messaggio è stato ripetuto)
/// </summary>
public int RepeatCount { get; set; } = 1;
/// <summary>
/// Formato compatto per display: solo ora con millisecondi
/// </summary>
public string TimeDisplay => Timestamp.ToString("HH:mm:ss.fff");
/// <summary>
/// Icona Bootstrap per il livello
/// </summary>
public string LevelIcon => Level switch
{
AuctionLogLevel.Error => "bi-x-circle-fill",
AuctionLogLevel.Warning => "bi-exclamation-triangle-fill",
AuctionLogLevel.Success => "bi-check-circle-fill",
AuctionLogLevel.Bid => "bi-hand-index-thumb-fill",
AuctionLogLevel.Strategy => "bi-shield-fill",
AuctionLogLevel.Timing => "bi-stopwatch-fill",
AuctionLogLevel.Debug => "bi-bug-fill",
_ => "bi-info-circle-fill"
};
/// <summary>
/// Classe CSS per il livello
/// </summary>
public string LevelClass => Level switch
{
AuctionLogLevel.Error => "alog-error",
AuctionLogLevel.Warning => "alog-warning",
AuctionLogLevel.Success => "alog-success",
AuctionLogLevel.Bid => "alog-bid",
AuctionLogLevel.Strategy => "alog-strategy",
AuctionLogLevel.Timing => "alog-timing",
AuctionLogLevel.Debug => "alog-debug",
_ => "alog-info"
};
/// <summary>
/// Label breve del livello
/// </summary>
public string LevelLabel => Level switch
{
AuctionLogLevel.Error => "ERR",
AuctionLogLevel.Warning => "WARN",
AuctionLogLevel.Success => "OK",
AuctionLogLevel.Bid => "BID",
AuctionLogLevel.Strategy => "STRAT",
AuctionLogLevel.Timing => "TIME",
AuctionLogLevel.Debug => "DBG",
_ => "INFO"
};
/// <summary>
/// Label della categoria
/// </summary>
public string CategoryLabel => Category switch
{
AuctionLogCategory.Ticker => "Ticker",
AuctionLogCategory.Price => "Prezzo",
AuctionLogCategory.Reset => "Reset",
AuctionLogCategory.BidAttempt => "Puntata",
AuctionLogCategory.BidResult => "Risultato",
AuctionLogCategory.Strategy => "Strategia",
AuctionLogCategory.Value => "Valore",
AuctionLogCategory.Competition => "Compet.",
AuctionLogCategory.Limit => "Limite",
AuctionLogCategory.Diagnostic => "Diagn.",
AuctionLogCategory.Status => "Stato",
AuctionLogCategory.Polling => "Poll",
_ => "Generale"
};
}
/// <summary>
/// Livello di gravità del log per-asta
/// </summary>
public enum AuctionLogLevel
{
Debug = 0,
Info = 1,
Timing = 2,
Strategy = 3,
Bid = 4,
Success = 5,
Warning = 6,
Error = 7
}
/// <summary>
/// Categoria del log per filtraggio e raggruppamento
/// </summary>
public enum AuctionLogCategory
{
General,
Ticker,
Price,
Reset,
BidAttempt,
BidResult,
Strategy,
Value,
Competition,
Limit,
Diagnostic,
Status,
Polling
}
}
+14 -1
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
+106
View File
@@ -0,0 +1,106 @@
using System;
namespace AutoBidder.Models
{
/// <summary>
/// Rappresenta un'asta visualizzata nel browser delle aste
/// Contiene informazioni base per la visualizzazione nella griglia
/// </summary>
public class BidooBrowserAuction
{
/// <summary>
/// ID univoco dell'asta
/// </summary>
public string AuctionId { get; set; } = "";
/// <summary>
/// URL completo dell'asta
/// </summary>
public string Url { get; set; } = "";
/// <summary>
/// Nome/titolo del prodotto
/// </summary>
public string Name { get; set; } = "";
/// <summary>
/// URL dell'immagine del prodotto
/// </summary>
public string ImageUrl { get; set; } = "";
/// <summary>
/// Prezzo attuale dell'asta in euro
/// </summary>
public decimal CurrentPrice { get; set; }
/// <summary>
/// Username dell'ultimo bidder
/// </summary>
public string LastBidder { get; set; } = "";
/// <summary>
/// Tempo rimanente in secondi
/// </summary>
public int RemainingSeconds { get; set; }
/// <summary>
/// Timer formattato (es: "00:08")
/// </summary>
public string TimerDisplay => $"{RemainingSeconds / 60:00}:{RemainingSeconds % 60:00}";
/// <summary>
/// Frequenza timer dell'asta (in secondi)
/// </summary>
public int TimerFrequency { get; set; } = 8;
/// <summary>
/// Prezzo "Compralo Subito"
/// </summary>
public decimal BuyNowPrice { get; set; }
/// <summary>
/// Indica se l'asta è già stata aggiunta al monitor
/// </summary>
public bool IsMonitored { get; set; }
/// <summary>
/// Indica se l'asta è attiva (non chiusa)
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// Indica se l'asta è venduta
/// </summary>
public bool IsSold { get; set; }
/// <summary>
/// Indica se l'asta richiede solo puntate manuali (no autobid)
/// </summary>
public bool IsManualOnly { get; set; }
/// <summary>
/// Indica se è un'asta turbo (timer < 10 sec)
/// </summary>
public bool IsTurbo => TimerFrequency <= 8;
/// <summary>
/// ID del prodotto
/// </summary>
public int ProductId { get; set; }
/// <summary>
/// Indica se l'asta è un'asta di puntate/crediti
/// </summary>
public bool IsCreditAuction { get; set; }
/// <summary>
/// Valore crediti se è un'asta di puntate
/// </summary>
public int CreditValue { get; set; }
/// <summary>
/// Timestamp ultimo aggiornamento stato
/// </summary>
public DateTime LastUpdated { get; set; } = DateTime.UtcNow;
}
}
+40
View File
@@ -0,0 +1,40 @@
namespace AutoBidder.Models
{
/// <summary>
/// Rappresenta una categoria/scheda di aste su Bidoo
/// </summary>
public class BidooCategoryInfo
{
/// <summary>
/// ID del tab (es: 1, 2, 3, 4, 5)
/// </summary>
public int TabId { get; set; }
/// <summary>
/// ID del tag per le categorie specifiche (es: 6=Buoni, 5=Smartphone)
/// </summary>
public int TagId { get; set; }
/// <summary>
/// Slug della categoria (es: "buoni", "smartphone")
/// </summary>
public string Slug { get; set; } = "";
/// <summary>
/// Nome visualizzato della categoria
/// </summary>
public string DisplayName { get; set; } = "";
/// <summary>
/// Indica se questa categoria è una categoria speciale (preferite, tutte, puntate, manuali)
/// </summary>
public bool IsSpecialCategory { get; set; }
/// <summary>
/// Icona da mostrare (opzionale)
/// </summary>
public string? Icon { get; set; }
public override string ToString() => DisplayName;
}
}
+192
View File
@@ -0,0 +1,192 @@
namespace AutoBidder.Models
{
/// <summary>
/// Record per le statistiche aggregate di un prodotto nel database
/// </summary>
public class ProductStatisticsRecord
{
public string ProductKey { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
// Contatori
public int TotalAuctions { get; set; }
public int WonAuctions { get; set; }
public int LostAuctions { get; set; }
// Statistiche prezzo
public double AvgFinalPrice { get; set; }
public double? MinFinalPrice { get; set; }
public double? MaxFinalPrice { get; set; }
public double? MedianFinalPrice { get; set; }
// Statistiche puntate
public double AvgBidsToWin { get; set; }
public int? MinBidsToWin { get; set; }
public int? MaxBidsToWin { get; set; }
// Statistiche reset
public double AvgResets { get; set; }
public int? MinResets { get; set; }
public int? MaxResets { get; set; }
// Limiti consigliati (calcolati dall'algoritmo)
public double? RecommendedMinPrice { get; set; }
public double? RecommendedMaxPrice { get; set; }
public int? RecommendedMinResets { get; set; }
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; }
/// <summary>
/// Se true, usa i limiti personalizzati del prodotto. Se false, usa i globali.
/// </summary>
public bool UseCustomLimits { get; set; }
// JSON con statistiche per fascia oraria
public string? HourlyStatsJson { get; set; }
// Metadata
public string? LastUpdated { get; set; }
/// <summary>
/// Calcola il win rate come percentuale
/// </summary>
public double WinRate => TotalAuctions > 0 ? (double)WonAuctions / TotalAuctions * 100 : 0;
}
/// <summary>
/// Risultato asta esteso con tutti i campi per analytics
/// </summary>
public class AuctionResultExtended
{
public int Id { get; set; }
public string AuctionId { get; set; } = "";
public string AuctionName { get; set; } = "";
public double FinalPrice { get; set; }
public int BidsUsed { get; set; }
public bool Won { get; set; }
public string Timestamp { get; set; } = "";
public double? BuyNowPrice { get; set; }
public double? ShippingCost { get; set; }
public double? TotalCost { get; set; }
public double? Savings { get; set; }
// Campi estesi per analytics
public string? WinnerUsername { get; set; }
public int? ClosedAtHour { get; set; }
public string? ProductKey { get; set; }
public int? TotalResets { get; set; }
public int? WinnerBidsUsed { get; set; }
}
/// <summary>
/// Limiti consigliati per un'asta basati sulle statistiche storiche
/// </summary>
public class RecommendedLimits
{
public double MinPrice { get; set; }
public double MaxPrice { get; set; }
public int MinResets { get; set; }
public int MaxResets { get; set; }
public int MaxBids { get; set; }
/// <summary>
/// Confidence score (0-100) - quanto sono affidabili questi limiti
/// </summary>
public int ConfidenceScore { get; set; }
/// <summary>
/// Numero di aste usate per calcolare i limiti
/// </summary>
public int SampleSize { get; set; }
/// <summary>
/// Fascia oraria migliore per vincere (0-23)
/// </summary>
public int? BestHourToPlay { get; set; }
/// <summary>
/// Win rate medio per questo prodotto
/// </summary>
public double? AverageWinRate { get; set; }
}
/// <summary>
/// Statistiche per fascia oraria
/// </summary>
public class HourlyStats
{
public int Hour { get; set; }
public int TotalAuctions { get; set; }
public int WonAuctions { get; set; }
public double AvgFinalPrice { get; set; }
public double AvgBidsUsed { get; set; }
public double WinRate => TotalAuctions > 0 ? (double)WonAuctions / TotalAuctions * 100 : 0;
}
/// <summary>
/// Record completo storia asta con tutte le metriche avanzate
/// </summary>
public class CompleteAuctionHistoryRecord
{
public int Id { get; set; }
public string AuctionId { get; set; } = "";
public string AuctionName { get; set; } = "";
public string? ProductKey { get; set; }
public string? OriginalUrl { get; set; }
// Dati finali
public double FinalPrice { get; set; }
public double? BuyNowPrice { get; set; }
public double? ShippingCost { get; set; }
public double? TotalCost { get; set; }
public double? Savings { get; set; }
public double? SavingsPercentage { get; set; }
// Risultato
public bool Won { get; set; }
public string? WinnerUsername { get; set; }
public int? WinnerBidsUsed { get; set; }
// Metriche competizione
public int TotalResets { get; set; }
public int TotalUniqueBidders { get; set; }
public int MaxHeatMetric { get; set; }
public double AvgHeatMetric { get; set; }
public int TotalCollisions { get; set; }
// Mie statistiche
public int MyBidsUsed { get; set; }
public int MySuccessfulBids { get; set; }
public int MyFailedBids { get; set; }
public int MyTimerExpired { get; set; }
public double? MyAvgLatencyMs { get; set; }
// Timestamps
public DateTime ClosedAt { get; set; }
public int ClosedAtHour { get; set; }
public int? DurationSeconds { get; set; }
public bool IsCompleteTracking { get; set; }
// JSON
public string? AggressiveBiddersJson { get; set; }
public string? BiddersSummaryJson { get; set; }
// Proprietà calcolate
public string DurationFormatted => DurationSeconds.HasValue
? TimeSpan.FromSeconds(DurationSeconds.Value).ToString(@"hh\:mm\:ss")
: "-";
public double SuccessRate => (MySuccessfulBids + MyFailedBids) > 0
? (double)MySuccessfulBids / (MySuccessfulBids + MyFailedBids) * 100
: 0;
}
}
-250
View File
@@ -1,250 +0,0 @@
# ?? Nuovo Workflow Docker + Gitea - RIEPILOGO
## ? Cosa è Cambiato
### PRIMA (Approccio Custom)
- Profili `.pubxml` con comandi Docker custom
- Non compatibili con GUI Visual Studio
- Richiedeva comandi manuali da terminale
### DOPO (Approccio Nativo Visual Studio)
- Profili `.pubxml` standard Docker di Visual Studio
- **Funziona dalla GUI** (Tasto destro ? Pubblica)
- Post-build target automatico nel `.csproj`
- Workflow completamente integrato
---
## ?? Workflow Completo
```
???????????????????????????????????????????????????
? Visual Studio ? Tasto Destro ? Pubblica ?
? Seleziona profilo: GiteaRegistry ?
???????????????????????????????????????????????????
?
?
???????????????????????????????????????????????????
? 1. Build .NET (Release) ?
???????????????????????????????????????????????????
?
?
???????????????????????????????????????????????????
? 2. Docker build ?
? docker build -t autobidder:latest . ?
???????????????????????????????????????????????????
?
?
???????????????????????????????????????????????????
? 3. POST-BUILD TARGET (AutoBidder.csproj) ?
? - Tag: autobidder:latest ?
? ? gitea.../alby96/autobidder:latest ?
? - Tag: autobidder:latest ?
? ? gitea.../alby96/autobidder:1.0.0 ?
???????????????????????????????????????????????????
?
?
???????????????????????????????????????????????????
? 4. Push su Gitea ?
? - docker push .../autobidder:latest ?
? - docker push .../autobidder:1.0.0 ?
???????????????????????????????????????????????????
?
?
???????????????????????????????????????????????????
? ? PUBBLICATO SU GITEA ?
? https://gitea.../Alby96/-/packages ?
???????????????????????????????????????????????????
```
---
## ?? File Modificati
### 1. `AutoBidder.csproj`
**Aggiunto:**
```xml
<!-- POST-BUILD TARGET: Push automatico su Gitea -->
<Target Name="PushDockerImageToGitea" AfterTargets="Publish" Condition="'$(PushToGiteaRegistry)' == 'true'">
<!-- Tag e push automatico su gitea.encke-hake.ts.net/alby96/autobidder -->
</Target>
```
### 2. `Properties/PublishProfiles/GiteaRegistry.pubxml` (NUOVO)
```xml
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<WebPublishMethod>Docker</WebPublishMethod>
<DockerPublish>true</DockerPublish>
<PublishProvider>DockerContainer</PublishProvider>
<DockerfileTag>autobidder:latest</DockerfileTag>
<PushToGiteaRegistry>true</PushToGiteaRegistry> <!-- Abilita push -->
</PropertyGroup>
</Project>
```
**Cosa fa:**
- Build Docker dell'immagine locale
- Attiva post-build target per push su Gitea
- **Funziona da GUI Visual Studio** ?
### 3. `Properties/PublishProfiles/GiteaRegistry-LocalOnly.pubxml` (NUOVO)
```xml
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<WebPublishMethod>Docker</WebPublishMethod>
<DockerPublish>true</DockerPublish>
<PublishProvider>DockerContainer</PublishProvider>
<DockerfileTag>autobidder:latest</DockerfileTag>
<PushToGiteaRegistry>false</PushToGiteaRegistry> <!-- NO push -->
</PropertyGroup>
</Project>
```
**Cosa fa:**
- Build Docker solo locale
- NESSUN push su Gitea
- Utile per test
### 4. `DOCKER_PUBLISH_GUIDE.md` (AGGIORNATA)
- Istruzioni per uso da Visual Studio GUI
- Workflow completo documentato
- Troubleshooting aggiornato
---
## ?? Come Usare
### Opzione 1: Da Visual Studio (CONSIGLIATO)
1. **Tasto destro** sul progetto `AutoBidder`
2. Click **Pubblica**
3. Seleziona profilo: **`GiteaRegistry`**
4. Click **Pubblica**
? **FATTO!** L'immagine viene buildat?, taggata e pubblicata automaticamente.
### Opzione 2: Da Riga di Comando
```bash
dotnet publish -c Release /p:PublishProfile=GiteaRegistry
```
### Opzione 3: Solo Build Locale (Test)
```bash
dotnet publish -c Release /p:PublishProfile=GiteaRegistry-LocalOnly
```
---
## ?? Prerequisito: Autenticazione
**Prima volta (OBBLIGATORIO):**
```bash
# 1. Genera Token PAT su Gitea
# https://gitea.encke-hake.ts.net/user/settings/applications
# Scope: read:packages + write:packages
# 2. Autentica Docker
docker login gitea.encke-hake.ts.net
# Username: Alby96
# Password: [TOKEN PAT]
```
**NOTA:** Se hai 2FA su Gitea, il Token PAT è **OBBLIGATORIO**.
---
## ? Vantaggi del Nuovo Approccio
| Aspetto | Prima (Custom) | Dopo (Nativo VS) |
|---------|----------------|------------------|
| **GUI Visual Studio** | ? Non funzionava | ? Funziona perfettamente |
| **Semplicità** | Comandi manuali | Click ? Pubblica |
| **Standard** | Approccio custom | Standard Microsoft |
| **Manutenibilità** | Complesso | Semplice |
| **Errori** | Difficili da debuggare | Output chiaro |
| **Workflow** | Multi-step manuale | Automatico end-to-end |
---
## ?? Verifica Post-Pubblicazione
Dopo la pubblicazione, Visual Studio mostrerà:
```
========================================
POST-BUILD: Tagging and pushing to Gitea Registry
========================================
Tagged: gitea.encke-hake.ts.net/alby96/autobidder:latest
Tagged: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
========================================
Pushing to Gitea Registry...
========================================
Pushed: gitea.encke-hake.ts.net/alby96/autobidder:latest
Pushed: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
========================================
SUCCESS: Images published to Gitea!
========================================
View on Gitea:
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder/latest
========================================
```
**Verifica su Gitea:**
- Vai su: `https://gitea.encke-hake.ts.net/Alby96/-/packages`
- Cerca package: `autobidder` (tipo: Container)
- Verifica tag: `latest` e `1.0.0`
- Controlla data: dovrebbe essere oggi
---
## ?? Prossimi Passi
1. ? Autenticati con Docker (Token PAT)
2. ? Prova pubblicazione: Tasto destro ? Pubblica ? GiteaRegistry
3. ? Verifica su Gitea che l'immagine sia caricata
4. ? Deploy su Unraid/altro server
---
## ?? Note Importanti
### Convenzione Nomi Gitea (CORRETTA)
```
gitea.encke-hake.ts.net/alby96/autobidder:latest
??????????????????????? ?????? ???????????
registro owner immagine
? 3 LIVELLI (corretto)
? Non usare: /alby96/mimante/autobidder (4 livelli - ERRATO)
```
### Post-Build Condition
Il post-build target si attiva **SOLO** se:
- Profilo ha `<PushToGiteaRegistry>true</PushToGiteaRegistry>`
- `GiteaRegistry.pubxml` ? Push attivato ?
- `GiteaRegistry-LocalOnly.pubxml` ? Push disabilitato ?
### Aggiornamento Versione
Per pubblicare nuova versione:
1. Modifica `<Version>1.0.1</Version>` in `AutoBidder.csproj`
2. Pubblica normalmente
3. Vengono creati tag: `latest` (aggiornato) + `1.0.1` (nuovo)
---
**? CONFIGURAZIONE COMPLETATA!**
Ora hai un workflow professionale integrato con Visual Studio per pubblicare su Gitea! ??
-274
View File
@@ -1,274 +0,0 @@
# ?? Problema HTTPS in Docker - RISOLTO
## ? Errore Originale
```
Unhandled exception. System.InvalidOperationException:
Unable to configure HTTPS endpoint. No server certificate was specified,
and the default developer certificate could not be found or is out of date.
To generate a developer certificate run 'dotnet dev-certs https'.
To trust the certificate (Windows and macOS only) run 'dotnet dev-certs https --trust'.
at Program.<>c.<<Main>$>b__0_6(ListenOptions listenOptions) in /src/Program.cs:line 17
```
## ?? Analisi del Problema
### Causa
**Nel `Program.cs` (versione precedente):**
```csharp
// PROBLEMA: In Development, enableHttps = true
var enableHttps = builder.Configuration.GetValue<bool>(
"Kestrel:EnableHttps",
builder.Environment.IsDevelopment() // ? true in Dev!
);
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(5000); // HTTP
if (enableHttps)
{
options.ListenAnyIP(5001, listenOptions =>
{
// ? Cerca certificato che non esiste in container!
listenOptions.UseHttps();
});
}
});
```
**Problema:**
- In ambiente `Development` (o assente), `enableHttps = true`
- In Docker, `ASPNETCORE_ENVIRONMENT=Production` ma il certificato non esiste
- Kestrel fallisce all'avvio cercando certificati di sviluppo
### Flusso Errore
```
1. Docker build ? ASPNETCORE_ENVIRONMENT=Production
2. Program.cs ? IsDevelopment() = false
3. Ma se Kestrel:EnableHttps non è settato ? usa default
4. In alcune configurazioni, tenta comunque HTTPS
5. listenOptions.UseHttps() ? cerca certificato
6. Certificato non trovato ? CRASH! ?
```
---
## ? Soluzione Implementata
### 1. Modifica `Program.cs`
```csharp
// ? CORRETTO: HTTPS disabilitato di default
var enableHttps = builder.Configuration.GetValue<bool>("Kestrel:EnableHttps", false);
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(8080); // HTTP porta standard container
if (enableHttps)
{
try
{
// Cerca certificato esplicito da configurazione
var certPath = builder.Configuration["Kestrel:Certificates:Default:Path"];
var certPassword = builder.Configuration["Kestrel:Certificates:Default:Password"];
if (!string.IsNullOrEmpty(certPath) && File.Exists(certPath))
{
// Usa certificato fornito (production con cert)
options.ListenAnyIP(8443, listenOptions =>
{
listenOptions.UseHttps(certPath, certPassword);
});
}
else if (builder.Environment.IsDevelopment())
{
// Certificato dev SOLO se esplicitamente Development
options.ListenAnyIP(5001, listenOptions =>
{
listenOptions.UseHttps();
});
}
else
{
Console.WriteLine("[Kestrel] HTTPS requested but no certificate found");
Console.WriteLine("[Kestrel] Running in HTTP-only mode");
}
}
catch (Exception ex)
{
Console.WriteLine($"[Kestrel] Failed to enable HTTPS: {ex.Message}");
Console.WriteLine("[Kestrel] Running in HTTP-only mode");
}
}
else
{
Console.WriteLine("[Kestrel] HTTPS disabled - running in HTTP-only mode");
Console.WriteLine("[Kestrel] Use a reverse proxy for SSL termination");
}
});
```
### 2. Modifica `Dockerfile`
```dockerfile
# Environment variables
ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production
ENV Kestrel__EnableHttps=false # ? Disabilita HTTPS esplicitamente
```
### 3. Porta Cambiata
- ? Prima: `5000` (HTTP) + `5001` (HTTPS)
- ? Dopo: `8080` (HTTP standard container)
---
## ?? Confronto Prima/Dopo
| Aspetto | Prima | Dopo |
|---------|-------|------|
| **Default HTTPS** | ? Abilitato in Dev | ? Disabilitato |
| **Porta HTTP** | 5000 | 8080 (standard) |
| **Porta HTTPS** | 5001 (fallisce) | 8443 (opzionale) |
| **Certificato** | Richiesto | Opzionale |
| **Crash startup** | ? Sì | ? No |
| **Reverse proxy** | N/A | ? Consigliato |
---
## ?? Best Practices per HTTPS in Container
### ? NON FARE (Anti-pattern)
```dockerfile
# ? SBAGLIATO: Abilita HTTPS senza certificato
ENV ASPNETCORE_URLS=https://+:5001
```
### ? PATTERN CORRETTO
**Opzione 1: HTTP Only + Reverse Proxy (CONSIGLIATO)**
```dockerfile
# Container espone solo HTTP
ENV ASPNETCORE_URLS=http://+:8080
ENV Kestrel__EnableHttps=false
```
```nginx
# Nginx gestisce SSL
server {
listen 443 ssl;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
location / {
proxy_pass http://autobidder:8080;
}
}
```
**Opzione 2: HTTPS con Certificato nel Container**
```bash
docker run -d \
-e Kestrel__EnableHttps=true \
-e Kestrel__Certificates__Default__Path=/certs/cert.pfx \
-e Kestrel__Certificates__Default__Password=password \
-v /host/certs:/certs \
-p 443:8443 \
autobidder:latest
```
---
## ?? Test Correzione
### Prima (ERRORE)
```bash
docker run -p 5000:5000 autobidder:latest
# System.InvalidOperationException: Unable to configure HTTPS endpoint
# ? Container CRASH!
```
### Dopo (SUCCESS)
```bash
docker run -p 5000:8080 autobidder:latest
# [Kestrel] HTTPS disabled - running in HTTP-only mode
# [Kestrel] Use a reverse proxy for SSL termination
# ? Application started successfully!
```
### Verifica
```bash
# Container in esecuzione
docker ps
# CONTAINER ID IMAGE PORTS
# abc123 autobidder:latest 0.0.0.0:5000->8080/tcp
# Test endpoint
curl http://localhost:5000
# ? Risposta OK!
```
---
## ?? Configurazione Unraid/Docker Compose
### Docker Compose
```yaml
version: '3.8'
services:
autobidder:
image: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
container_name: autobidder
ports:
- "5000:8080" # Host:Container
volumes:
- ./data:/app/Data
- ./logs:/app/logs
environment:
- ASPNETCORE_ENVIRONMENT=Production
- Kestrel__EnableHttps=false
restart: unless-stopped
```
### Unraid Template
```
Repository: gitea.encke-hake.ts.net/alby96/autobidder:latest
Port: 5000 (host) ? 8080 (container) [HTTP]
Volume: /mnt/user/appdata/autobidder/data ? /app/Data
Volume: /mnt/user/appdata/autobidder/logs ? /app/logs
Environment: ASPNETCORE_ENVIRONMENT=Production
Environment: Kestrel__EnableHttps=false
```
---
## ? Checklist Finale
- [x] HTTPS disabilitato di default in container
- [x] Porta HTTP cambiata da 5000 ? 8080 (standard)
- [x] Dockerfile aggiornato con `Kestrel__EnableHttps=false`
- [x] Program.cs modificato per gestire correttamente HTTPS opzionale
- [x] Certificati di sviluppo SOLO in ambiente Development
- [x] Reverse proxy consigliato per SSL in production
- [x] Documentazione aggiornata
- [x] Container si avvia senza errori
**PROBLEMA RISOLTO!** ??
Container ora si avvia correttamente in modalità HTTP-only, pronto per reverse proxy SSL in production.
-214
View File
@@ -1,214 +0,0 @@
# ?? PROBLEMA RISOLTO: Errore Visual Studio con Push Riuscito
## ?? Analisi del Problema
### ? Cosa Funzionava
Dal log di pubblicazione:
```
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:latest
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
docker push gitea.encke-hake.ts.net/alby96/autobidder:latest
...
latest: digest: sha256:dc08591c525e29d881f65effbc569a1c4c75d7d43614d75231e9c8035e3865b0 size: 856
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:latest
docker push gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
...
1.0.0: digest: sha256:dc08591c525e29d881f65effbc569a1c4c75d7d43614d75231e9c8035e3865b0 size: 856
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
?????????????????????????????????????????????????????????????????????
? ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ?
?????????????????????????????????????????????????????????????????????
```
**Tutto perfetto**: Build, tag e push su Gitea funzionanti al 100%!
### ? Errore Visual Studio
Alla fine del processo:
```
1>La compilazione non è riuscita. Vedere la finestra di output per altre informazioni.
========== Pubblicazione: 0 completato/i, 1 non riuscito/i, 0 ignorato/i ==========
Errore MSB4057: la destinazione "ContainerBuild" non è presente nel progetto.
C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Sdks\Microsoft.Docker.Sdk\build\Microsoft.Docker.targets(173,5)
```
---
## ?? Causa del Problema
### Configurazione Precedente (ERRATA)
**File:** `Properties/PublishProfiles/GiteaRegistry.pubxml`
```xml
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!-- ? PROBLEMA: Usa Docker SDK di Visual Studio -->
<WebPublishMethod>Docker</WebPublishMethod>
<DockerPublish>true</DockerPublish>
<PublishProvider>DockerContainer</PublishProvider>
<_TargetId>Docker</_TargetId>
<DockerfileTag>autobidder:latest</DockerfileTag>
<PushToGiteaRegistry>true</PushToGiteaRegistry>
</PropertyGroup>
</Project>
```
**Problema:**
- `<WebPublishMethod>Docker</WebPublishMethod>` richiede **Microsoft.Docker.Sdk**
- Visual Studio cerca il target `ContainerBuild` nel progetto
- Il target non esiste perché l'SDK non è installato (e non serve!)
- Visual Studio fallisce DOPO che il nostro workflow custom ha già pubblicato con successo
### Flusso Esecuzione
```
1. ? Build .NET (Release)
2. ? Publish files ? obj\Docker\publish
3. ? Post-build target "PushDockerImageToGitea" (dal .csproj)
?? ? docker build
?? ? docker tag
?? ? docker push (SUCCESSO!)
4. ? Visual Studio cerca target "ContainerBuild" (Docker SDK)
5. ? Target non trovato ? ERRORE (ma push già fatto!)
```
---
## ? Soluzione Implementata
### Nuova Configurazione (CORRETTA)
**File:** `Properties/PublishProfiles/GiteaRegistry.pubxml`
```xml
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!-- ? CORRETTO: Usa Custom senza Docker SDK -->
<WebPublishMethod>Custom</WebPublishMethod>
<PublishProvider>FileSystem</PublishProvider>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<!-- Path pubblicazione temporanea -->
<PublishUrl>obj\Docker\publish</PublishUrl>
<DeleteExistingFiles>True</DeleteExistingFiles>
<!-- Configurazione Docker -->
<DockerfileTag>autobidder:latest</DockerfileTag>
<DockerfilePath>$(MSBuildProjectDirectory)\Dockerfile</DockerfilePath>
<DockerfileContext>$(MSBuildProjectDirectory)</DockerfileContext>
<!-- Abilita post-build Gitea -->
<PushToGiteaRegistry>true</PushToGiteaRegistry>
</PropertyGroup>
<!-- Target Docker Build custom -->
<Target Name="DockerBuild" AfterTargets="GatherAllFilesToPublish">
<Message Importance="high" Text="?? Building Docker image..." />
<Exec Command="docker build -t $(DockerfileTag) -f &quot;$(DockerfilePath)&quot; &quot;$(DockerfileContext)&quot;" />
<Message Importance="high" Text="? Docker build completed!" />
</Target>
</Project>
```
**Vantaggi:**
- ? Non richiede Microsoft.Docker.Sdk
- ? Visual Studio non cerca target mancanti
- ? Controllo completo del workflow
- ? Nessun errore alla fine del processo
### Nuovo Flusso Esecuzione
```
1. ? Build .NET (Release)
2. ? Publish files ? obj\Docker\publish
3. ? Target "DockerBuild" (dal profilo .pubxml)
?? docker build -t autobidder:latest
4. ? Post-build target "PushDockerImageToGitea" (dal .csproj)
?? docker tag ? gitea.../autobidder:latest
?? docker tag ? gitea.../autobidder:1.0.0
?? docker push latest
?? docker push 1.0.0
5. ? Visual Studio: SUCCESS! ?
```
---
## ?? Confronto Prima/Dopo
| Aspetto | Prima (Docker SDK) | Dopo (Custom) |
|---------|-------------------|---------------|
| **WebPublishMethod** | `Docker` | `Custom` |
| **Richiede SDK** | ? Sì (non installato) | ? No |
| **Docker Build** | Post-build .csproj | Target .pubxml + Post-build |
| **Errore finale** | ? Sì (target mancante) | ? No |
| **Push funziona** | ? Sì | ? Sì |
| **Visual Studio OK** | ? No (errore) | ? Sì |
---
## ?? Risultato Finale
### Output Pubblicazione Corretta
```
?????????????????????????????????????????????????????????????????????
? DOCKER BUILD: Building container image ?
?????????????????????????????????????????????????????????????????????
?? Building: autobidder:latest
? Docker build completed successfully!
?????????????????????????????????????????????????????????????????????
? POST-BUILD: Pubblicazione su Gitea Container Registry ?
?????????????????????????????????????????????????????????????????????
?? Solution Version: 1.0.0
??? Target Tags:
• gitea.encke-hake.ts.net/alby96/autobidder:latest
• gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:latest
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:latest
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
?????????????????????????????????????????????????????????????????????
? ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ?
?????????????????????????????????????????????????????????????????????
========== Pubblicazione: 1 completato/i, 0 non riuscito/i, 0 ignorato/i ==========
```
**Visual Studio mostra SUCCESS senza errori!** ?
---
## ?? Lezioni Apprese
1. **`WebPublishMethod=Docker`** richiede Microsoft.Docker.Sdk installato
2. **`WebPublishMethod=Custom`** permette workflow personalizzati senza SDK
3. Il nostro workflow custom funzionava già (push riuscito), ma Visual Studio non era soddisfatto
4. Separare build Docker (target nel .pubxml) da push Gitea (target nel .csproj) rende il processo più chiaro
5. Visual Studio può mostrare errori anche se l'operazione è riuscita (cerca target che non trova)
---
## ? Checklist Verifica
- [x] Build .NET funziona
- [x] Docker build funziona
- [x] Tag Gitea creati
- [x] Push su Gitea riuscito
- [x] Visual Studio non mostra errori
- [x] Digest SHA256 visibile su Gitea
- [x] Immagini disponibili per pull
**TUTTO FUNZIONANTE!** ??
+206
View File
@@ -0,0 +1,206 @@
@page
@model AutoBidder.Pages.Account.LoginModel
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Login - AutoBidder</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.login-card {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
padding: 40px;
width: 100%;
max-width: 380px;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3);
}
.login-header {
text-align: center;
margin-bottom: 35px;
}
.login-header h1 {
color: #fff;
font-size: 28px;
font-weight: 600;
margin-bottom: 8px;
}
.login-header p {
color: rgba(255, 255, 255, 0.6);
font-size: 14px;
}
.form-floating {
margin-bottom: 20px;
}
.form-floating .form-control {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 12px;
color: #fff;
height: 55px;
padding: 16px;
}
.form-floating .form-control:focus {
background: rgba(255, 255, 255, 0.12);
border-color: #4f46e5;
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.25);
color: #fff;
}
.form-floating .form-control::placeholder {
color: transparent;
}
.form-floating label {
color: rgba(255, 255, 255, 0.5);
padding: 16px;
}
.form-floating .form-control:focus ~ label,
.form-floating .form-control:not(:placeholder-shown) ~ label {
color: rgba(255, 255, 255, 0.7);
}
.form-check {
margin-bottom: 25px;
}
.form-check-input {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.3);
}
.form-check-input:checked {
background-color: #4f46e5;
border-color: #4f46e5;
}
.form-check-label {
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
}
.btn-login {
width: 100%;
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
border: none;
border-radius: 12px;
color: #fff;
font-weight: 600;
font-size: 16px;
padding: 14px;
transition: all 0.3s ease;
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(79, 70, 229, 0.4);
color: #fff;
}
.btn-login:active {
transform: translateY(0);
}
.alert-error {
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 12px;
color: #fca5a5;
padding: 12px 16px;
margin-bottom: 20px;
font-size: 14px;
}
.login-footer {
text-align: center;
margin-top: 25px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.login-footer small {
color: rgba(255, 255, 255, 0.4);
font-size: 12px;
}
.login-footer i {
margin-right: 5px;
}
</style>
</head>
<body>
<div class="login-card">
<div class="login-header">
<h1>AutoBidder</h1>
<p>Sistema Gestione Aste Bidoo</p>
</div>
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div class="alert-error">
<i class="bi bi-exclamation-circle"></i>
@Model.ErrorMessage
</div>
}
<form method="post">
@Html.AntiForgeryToken()
<div class="form-floating">
<input type="text" class="form-control" id="username" name="Username"
placeholder="Username" value="@Model.Username" required autocomplete="username" />
<label for="username"><i class="bi bi-person"></i> Username</label>
</div>
<div class="form-floating">
<input type="password" class="form-control" id="password" name="Password"
placeholder="Password" required autocomplete="current-password" />
<label for="password"><i class="bi bi-lock"></i> Password</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="rememberMe" name="RememberMe" value="true" />
<label class="form-check-label" for="rememberMe">Ricordami</label>
</div>
<button type="submit" class="btn btn-login">
<i class="bi bi-box-arrow-in-right"></i> Accedi
</button>
</form>
<div class="login-footer">
<small><i class="bi bi-shield-lock"></i> Connessione sicura</small>
</div>
</div>
</body>
</html>
+89
View File
@@ -0,0 +1,89 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using AutoBidder.Models;
namespace AutoBidder.Pages.Account;
public class LoginModel : PageModel
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
public LoginModel(SignInManager<ApplicationUser> signInManager, UserManager<ApplicationUser> userManager)
{
_signInManager = signInManager;
_userManager = userManager;
}
[BindProperty]
public string Username { get; set; } = string.Empty;
[BindProperty]
public string Password { get; set; } = string.Empty;
[BindProperty]
public bool RememberMe { get; set; }
public string? ErrorMessage { get; set; }
[FromQuery(Name = "returnUrl")]
public string? ReturnUrl { get; set; }
public async Task<IActionResult> OnGetAsync()
{
// Se già autenticato, vai alla home
if (User.Identity?.IsAuthenticated == true)
{
return LocalRedirect(GetSafeReturnUrl());
}
// Logout eventuali sessioni precedenti
await HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme);
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password))
{
ErrorMessage = "Inserisci username e password.";
return Page();
}
var result = await _signInManager.PasswordSignInAsync(
Username,
Password,
RememberMe,
lockoutOnFailure: true
);
if (result.Succeeded)
{
return LocalRedirect(GetSafeReturnUrl());
}
if (result.IsLockedOut)
{
ErrorMessage = "Account bloccato. Riprova tra qualche minuto.";
}
else
{
ErrorMessage = "Username o password non validi.";
}
return Page();
}
private string GetSafeReturnUrl()
{
// Ritorna solo URL locali sicuri
if (!string.IsNullOrEmpty(ReturnUrl) && Url.IsLocalUrl(ReturnUrl))
{
return ReturnUrl;
}
return "/";
}
}
+5
View File
@@ -0,0 +1,5 @@
@page
@model AutoBidder.Pages.Account.LogoutModel
@{
Layout = null;
}
+21
View File
@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AutoBidder.Pages.Account;
public class LogoutModel : PageModel
{
public async Task<IActionResult> OnGetAsync()
{
await HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme);
return Redirect("/Account/Login");
}
public async Task<IActionResult> OnPostAsync()
{
await HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme);
return Redirect("/Account/Login");
}
}
+671
View File
@@ -0,0 +1,671 @@
@page "/browser"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@using AutoBidder.Models
@using AutoBidder.Services
@inject BidooBrowserService BrowserService
@inject ApplicationStateService AppState
@inject AuctionMonitor AuctionMonitor
@inject IJSRuntime JSRuntime
@implements IDisposable
<PageTitle>Esplora Aste - AutoBidder</PageTitle>
<div class="browser-container animate-fade-in p-4">
<!-- Header -->
<div class="d-flex align-items-center justify-content-between mb-4 flex-wrap gap-3">
<div class="d-flex align-items-center animate-fade-in-down">
<i class="bi bi-search text-primary me-3" style="font-size: 2rem;"></i>
<div>
<h2 class="mb-0 fw-bold">Esplora Aste</h2>
<small class="text-muted">Naviga le aste pubbliche di Bidoo senza login</small>
</div>
</div>
<div class="d-flex gap-2 flex-wrap">
<button class="btn btn-outline-secondary" @onclick="RefreshAll" disabled="@isLoading">
<i class="bi @(isLoading ? "bi-arrow-clockwise spin" : "bi-arrow-clockwise")"></i>
Aggiorna
</button>
@if (auctions.Count > 0)
{
<button class="btn btn-outline-danger" @onclick="ClearAllAuctions" title="Rimuove tutte le aste caricate e ferma il polling">
<i class="bi bi-trash"></i>
Pulisci Tutto
</button>
}
</div>
</div>
<!-- Category Selector -->
<div class="card mb-4 shadow-sm">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-6">
<label class="form-label fw-semibold">
<i class="bi bi-tag me-2"></i>Categoria
</label>
<select class="form-select form-select-lg" @bind="selectedCategoryIndex" @bind:after="OnCategoryChanged">
@if (categories.Count == 0)
{
<option value="-1">Caricamento categorie...</option>
}
else
{
@for (int i = 0; i < categories.Count; i++)
{
<option value="@i">
@if (!string.IsNullOrEmpty(categories[i].Icon))
{
@categories[i].DisplayName
}
else
{
@categories[i].DisplayName
}
</option>
}
}
</select>
</div>
<div class="col-md-3">
<div class="stats-mini">
<span class="text-muted">Aste caricate:</span>
<span class="fw-bold text-primary ms-2">@auctions.Count</span>
</div>
</div>
<div class="col-md-3">
<div class="stats-mini">
<span class="text-muted">Monitorate:</span>
<span class="fw-bold text-success ms-2">@auctions.Count(a => a.IsMonitored)</span>
</div>
</div>
</div>
</div>
</div>
<!-- ? NUOVO: Search Bar -->
<div class="card mb-4 shadow-sm">
<div class="card-body">
<div class="row g-3 align-items-center">
<div class="col-md-8">
<div class="input-group input-group-lg">
<span class="input-group-text bg-primary text-white border-0">
<i class="bi bi-search"></i>
</span>
<input type="text"
class="form-control form-control-lg border-0"
placeholder="Cerca per nome asta, prezzo, vincitore..."
@bind="searchQuery"
@bind:event="oninput"
@bind:after="OnSearchChanged" />
@if (!string.IsNullOrEmpty(searchQuery))
{
<button class="btn btn-outline-secondary border-0"
@onclick="ClearSearch"
title="Cancella ricerca">
<i class="bi bi-x-lg"></i>
</button>
}
</div>
</div>
<div class="col-md-4">
<div class="stats-mini">
<span class="text-muted">Risultati filtrati:</span>
<span class="fw-bold text-info ms-2">@filteredAuctions.Count</span>
</div>
</div>
</div>
</div>
</div>
<!-- Loading State -->
@if (isLoading)
{
<div class="text-center py-5 animate-fade-in">
<div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;">
<span class="visually-hidden">Caricamento...</span>
</div>
<p class="text-muted">Caricamento aste in corso...</p>
</div>
}
else if (errorMessage != null)
{
<div class="alert alert-warning d-flex align-items-center animate-scale-in">
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
<div>
<strong>Attenzione</strong><br />
@errorMessage
</div>
</div>
}
else if (filteredAuctions.Count == 0 && !string.IsNullOrEmpty(searchQuery))
{
<div class="text-center py-5 animate-fade-in">
<i class="bi bi-search text-muted" style="font-size: 4rem;"></i>
<p class="text-muted mt-3">Nessuna asta trovata per "<strong>@searchQuery</strong>"</p>
<button class="btn btn-outline-secondary" @onclick="ClearSearch">
<i class="bi bi-x-circle me-2"></i>Cancella Ricerca
</button>
</div>
}
else if (auctions.Count == 0)
{
<div class="text-center py-5 animate-fade-in">
<i class="bi bi-inbox text-muted" style="font-size: 4rem;"></i>
<p class="text-muted mt-3">Nessuna asta trovata in questa categoria</p>
<button class="btn btn-primary" @onclick="LoadAuctions">
<i class="bi bi-arrow-clockwise me-2"></i>Ricarica
</button>
</div>
}
else
{
<!-- Auctions Grid -->
<div class="auction-grid animate-fade-in">
@foreach (var auction in filteredAuctions)
{
<div class="auction-card @(auction.IsMonitored ? "monitored" : "") @(auction.IsSold ? "sold" : "")">
<!-- Image -->
<div class="auction-image">
@if (!string.IsNullOrEmpty(auction.ImageUrl))
{
<img src="@auction.ImageUrl" alt="@auction.Name" loading="lazy" />
}
else
{
<div class="placeholder-image">
<i class="bi bi-image"></i>
</div>
}
<!-- Badges -->
<div class="auction-badges">
@if (auction.IsCreditAuction)
{
<span class="badge bg-warning text-dark">
<i class="bi bi-coin"></i> @auction.CreditValue
</span>
}
@if (auction.IsManualOnly)
{
<span class="badge bg-info">
<i class="bi bi-hand-index"></i> Manuale
</span>
}
@if (auction.IsTurbo)
{
<span class="badge bg-danger">
<i class="bi bi-lightning"></i> @auction.TimerFrequency s
</span>
}
</div>
@if (auction.IsSold)
{
<div class="sold-overlay">
<span>VENDUTO</span>
</div>
}
@if (auction.IsMonitored)
{
<div class="monitored-badge">
<i class="bi bi-check-circle-fill"></i>
</div>
}
</div>
<!-- Info -->
<div class="auction-info">
<h6 class="auction-name" title="@auction.Name">@auction.Name</h6>
<div class="auction-price">
<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>
</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>
</div>
<div class="auction-timer @(auction.RemainingSeconds <= 3 ? "urgent" : "")">
<i class="bi bi-clock me-1"></i>
@auction.TimerDisplay
</div>
</div>
<!-- Actions -->
<div class="auction-actions">
<div class="d-flex gap-1 mb-1">
<button class="btn btn-outline-secondary btn-sm flex-grow-1"
@onclick="() => CopyAuctionLink(auction)"
title="Copia link">
<i class="bi bi-clipboard"></i>
</button>
<button class="btn btn-outline-secondary btn-sm flex-grow-1"
@onclick="() => OpenAuctionInNewTab(auction)"
title="Apri in nuova scheda">
<i class="bi bi-box-arrow-up-right"></i>
</button>
</div>
@if (auction.IsMonitored)
{
<button class="btn btn-warning btn-sm w-100" @onclick="() => RemoveFromMonitor(auction)">
<i class="bi bi-dash-lg me-1"></i>Togli dal Monitor
</button>
}
else
{
<button class="btn btn-primary btn-sm w-100" @onclick="() => AddToMonitor(auction)">
<i class="bi bi-plus-lg me-1"></i>Aggiungi al Monitor
</button>
}
</div>
</div>
}
</div>
<!-- Load More -->
@if (canLoadMore)
{
<div class="text-center mt-4">
<button class="btn btn-outline-primary btn-lg" @onclick="LoadMoreAuctions" disabled="@isLoadingMore">
@if (isLoadingMore)
{
<span class="spinner-border spinner-border-sm me-2"></span>
}
else
{
<i class="bi bi-plus-circle me-2"></i>
}
Carica Altre Aste
</button>
</div>
}
}
</div>
@code {
private List<BidooCategoryInfo> categories = new();
private List<BidooBrowserAuction> auctions = new();
private List<BidooBrowserAuction> filteredAuctions = new();
// 🔥 Usa stato persistente da AppState
private int selectedCategoryIndex
{
get => AppState.BrowserCategoryIndex;
set => AppState.BrowserCategoryIndex = value;
}
private int currentPage = 0;
private bool isLoading = false;
private bool isLoadingMore = false;
private bool canLoadMore = true;
private string? errorMessage = null;
// 🔥 Usa stato persistente per la ricerca
private string searchQuery
{
get => AppState.BrowserSearchQuery;
set => AppState.BrowserSearchQuery = value;
}
private System.Threading.Timer? stateUpdateTimer;
private CancellationTokenSource? cts;
private bool isUpdatingInBackground = false;
protected override async Task OnInitializedAsync()
{
await LoadCategories();
// 🔥 Se c'è una categoria salvata, carica le aste
if (categories.Count > 0)
{
// 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
stateUpdateTimer = new System.Threading.Timer(async _ =>
{
if (auctions.Count > 0 && !isUpdatingInBackground)
{
await UpdateAuctionStatesBackground();
}
}, null, TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(500));
}
private async Task LoadCategories()
{
try
{
categories = await BrowserService.GetCategoriesAsync();
}
catch (Exception ex)
{
Console.WriteLine($"[Browser] Error loading categories: {ex.Message}");
errorMessage = "Errore nel caricamento delle categorie";
}
}
private async Task OnCategoryChanged()
{
currentPage = 0;
canLoadMore = true;
auctions.Clear();
await LoadAuctions();
}
private async Task LoadAuctions()
{
if (categories.Count == 0 || selectedCategoryIndex < 0 || selectedCategoryIndex >= categories.Count)
return;
isLoading = true;
errorMessage = null;
cts?.Cancel();
cts = new CancellationTokenSource();
try
{
var category = categories[selectedCategoryIndex];
var newAuctions = await BrowserService.GetAuctionsAsync(category, currentPage, cts.Token);
auctions = newAuctions;
canLoadMore = newAuctions.Count >= 20; // Assume pagination at 20
// Mark already monitored auctions
UpdateMonitoredStatus();
// Get initial states
if (auctions.Count > 0)
{
await BrowserService.UpdateAuctionStatesAsync(auctions, cts.Token);
}
// ? NUOVO: Applica filtro ricerca
ApplySearchFilter();
}
catch (OperationCanceledException)
{
// Ignore cancellation
}
catch (Exception ex)
{
Console.WriteLine($"[Browser] Error loading auctions: {ex.Message}");
errorMessage = "Errore nel caricamento delle aste";
}
finally
{
isLoading = false;
StateHasChanged();
}
}
// ? NUOVO: Metodo per applicare il filtro di ricerca
private void ApplySearchFilter()
{
if (string.IsNullOrWhiteSpace(searchQuery))
{
filteredAuctions = auctions.ToList();
return;
}
var query = searchQuery.ToLowerInvariant().Trim();
filteredAuctions = auctions.Where(a =>
// Cerca nel nome
a.Name.ToLowerInvariant().Contains(query) ||
// Cerca nel prezzo corrente
a.CurrentPrice.ToString("F2").Contains(query) ||
// Cerca nel prezzo buy-now
(a.BuyNowPrice > 0 && a.BuyNowPrice.ToString("F2").Contains(query)) ||
// Cerca nel nome dell'ultimo puntatore
(!string.IsNullOrEmpty(a.LastBidder) && a.LastBidder.ToLowerInvariant().Contains(query)) ||
// Cerca nell'ID asta
a.AuctionId.Contains(query)
).ToList();
}
// ? NUOVO: Callback quando cambia la ricerca
private void OnSearchChanged()
{
ApplySearchFilter();
StateHasChanged();
}
// ? NUOVO: Pulisce la ricerca
private void ClearSearch()
{
searchQuery = "";
ApplySearchFilter();
StateHasChanged();
}
private async Task LoadMoreAuctions()
{
if (categories.Count == 0 || selectedCategoryIndex < 0 || auctions.Count == 0)
return;
isLoadingMore = true;
cts?.Cancel();
cts = new CancellationTokenSource();
try
{
var category = categories[selectedCategoryIndex];
var existingIds = auctions.Select(a => a.AuctionId).ToList();
// Usa GetMoreAuctionsAsync che evita duplicati
var newAuctions = await BrowserService.GetMoreAuctionsAsync(category, existingIds, cts.Token);
if (newAuctions.Count == 0)
{
canLoadMore = false;
}
else
{
auctions.AddRange(newAuctions);
UpdateMonitoredStatus();
// Aggiorna stati delle nuove aste
await BrowserService.UpdateAuctionStatesAsync(newAuctions, cts.Token);
// ? NUOVO: Riapplica filtro dopo caricamento
ApplySearchFilter();
}
}
catch (Exception ex)
{
Console.WriteLine($"[Browser] Error loading more auctions: {ex.Message}");
}
finally
{
isLoadingMore = false;
StateHasChanged();
}
}
private async Task UpdateAuctionStatesBackground()
{
if (isUpdatingInBackground) return;
isUpdatingInBackground = true;
try
{
await BrowserService.UpdateAuctionStatesAsync(auctions);
UpdateMonitoredStatus();
await InvokeAsync(StateHasChanged);
}
catch
{
// Ignore background errors
}
finally
{
isUpdatingInBackground = false;
}
}
private async Task RefreshAll()
{
await LoadCategories();
currentPage = 0;
canLoadMore = true;
auctions.Clear();
await LoadAuctions();
}
private void ClearAllAuctions()
{
// Cancella le aste e ferma il timer
cts?.Cancel();
auctions.Clear();
filteredAuctions.Clear();
StateHasChanged();
}
private void UpdateMonitoredStatus()
{
var monitoredIds = AppState.Auctions.Select(a => a.AuctionId).ToHashSet();
foreach (var auction in auctions)
{
auction.IsMonitored = monitoredIds.Contains(auction.AuctionId);
}
}
private void AddToMonitor(BidooBrowserAuction browserAuction)
{
if (browserAuction.IsMonitored) return;
// 🔥 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,
Name = browserAuction.Name,
OriginalUrl = browserAuction.Url,
BuyNowPrice = (double)browserAuction.BuyNowPrice,
// 🔥 Applica valori dalle impostazioni
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs,
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
MinPrice = settings.DefaultMinPrice,
MaxPrice = settings.DefaultMaxPrice,
MinResets = settings.DefaultMinResets,
MaxResets = settings.DefaultMaxResets,
// 🔥 Usa stato da impostazioni invece di hardcoded
IsActive = isActive,
IsPaused = isPaused,
AddedAt = DateTime.UtcNow
};
AppState.AddAuction(auctionInfo);
// ?? FIX CRITICO: Registra l'asta nel monitor!
AuctionMonitor.AddAuction(auctionInfo);
browserAuction.IsMonitored = true;
// Save to disk
AutoBidder.Utilities.PersistenceManager.SaveAuctions(AppState.Auctions.ToList());
// ?? FIX CRITICO: Avvia il monitor se non è già attivo
if (!AppState.IsMonitoringActive)
{
AuctionMonitor.Start();
AppState.IsMonitoringActive = true;
Console.WriteLine($"[AuctionBrowser] Monitor auto-started for paused auction: {auctionInfo.Name}");
}
StateHasChanged();
}
private void RemoveFromMonitor(BidooBrowserAuction browserAuction)
{
if (!browserAuction.IsMonitored) return;
// Trova l'asta nel monitor
var auctionToRemove = AppState.Auctions.FirstOrDefault(a => a.AuctionId == browserAuction.AuctionId);
if (auctionToRemove != null)
{
AppState.RemoveAuction(auctionToRemove);
browserAuction.IsMonitored = false;
// Save to disk
AutoBidder.Utilities.PersistenceManager.SaveAuctions(AppState.Auctions.ToList());
}
StateHasChanged();
}
private async Task CopyAuctionLink(BidooBrowserAuction auction)
{
try
{
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", auction.Url);
}
catch (Exception ex)
{
Console.WriteLine($"[Browser] Error copying link: {ex.Message}");
}
}
private async Task OpenAuctionInNewTab(BidooBrowserAuction auction)
{
try
{
await JSRuntime.InvokeVoidAsync("window.open", auction.Url, "_blank");
}
catch (Exception ex)
{
Console.WriteLine($"[Browser] Error opening link: {ex.Message}");
}
}
public void Dispose()
{
stateUpdateTimer?.Dispose();
cts?.Cancel();
cts?.Dispose();
}
}
-32
View File
@@ -1,32 +0,0 @@
@page "/freebids"
<PageTitle>Puntate Gratuite - AutoBidder</PageTitle>
<div class="freebids-container animate-fade-in p-4">
<div class="d-flex align-items-center mb-4 animate-fade-in-down">
<i class="bi bi-gift-fill text-warning me-3" style="font-size: 2.5rem;"></i>
<h2 class="mb-0 fw-bold">Puntate Gratuite</h2>
</div>
<!-- Feature Under Development Notice - Conciso -->
<div class="alert alert-info border-0 shadow-sm animate-scale-in">
<div class="d-flex align-items-center">
<i class="bi bi-tools me-3" style="font-size: 2rem;"></i>
<div class="flex-grow-1">
<h5 class="mb-2"><strong>Funzionalità in Sviluppo</strong></h5>
<p class="mb-0">
Sistema di rilevamento, raccolta e utilizzo automatico delle puntate gratuite di Bidoo.
<br />
<small class="text-muted">Disponibile in una prossima versione con statistiche dettagliate.</small>
</p>
</div>
</div>
</div>
</div>
<style>
.freebids-container {
max-width: 1200px;
margin: 0 auto;
}
</style>
+1
View File
@@ -1,4 +1,5 @@
@page "/health"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@inject DatabaseService DatabaseService
@inject AuctionMonitor AuctionMonitor
+544 -447
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4
View File
@@ -9,9 +9,13 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="~/" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet" />
<link href="css/app-wpf.css" rel="stylesheet" />
<link href="css/modern-pages.css" rel="stylesheet" />
<link href="css/animations.css" rel="stylesheet" />
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
+316 -210
View File
@@ -1,15 +1,25 @@
using AutoBidder.Services;
using AutoBidder.Services;
using AutoBidder.Data;
using AutoBidder.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.DataProtection;
using System.Data.Common;
var builder = WebApplication.CreateBuilder(args);
// FORCE ASPNETCORE_URLS to prevent any override
// Questo garantisce che il container ascolti SEMPRE sulla porta configurata
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_URLS")))
{
builder.WebHost.UseUrls("http://+:8080");
}
else
{
builder.WebHost.UseUrls(Environment.GetEnvironmentVariable("ASPNETCORE_URLS")!);
}
// Configura Kestrel solo per HTTPS opzionale
// HTTP è gestito da ASPNETCORE_URLS (default: http://+:8080 nel Dockerfile)
// HTTP è gestito da ASPNETCORE_URLS (default: http://+:8080 nel Dockerfile)
var enableHttps = builder.Configuration.GetValue<bool>("Kestrel:EnableHttps", false);
if (enableHttps)
@@ -56,19 +66,36 @@ else
{
Console.WriteLine("[Kestrel] HTTPS disabled - running in HTTP-only mode");
Console.WriteLine("[Kestrel] Use a reverse proxy (nginx/traefik) for SSL termination");
Console.WriteLine($"[Kestrel] Listening on: {builder.Configuration["ASPNETCORE_URLS"] ?? "http://+:8080"}");
Console.WriteLine($"[Kestrel] Listening on: {Environment.GetEnvironmentVariable("ASPNETCORE_URLS") ?? "http://+:8080"}");
}
// Add services to the container
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
// ============================================
// CONFIGURAZIONE DATABASE - Path configurabile via DATA_PATH
// ============================================
// Determina il path base per tutti i database e dati persistenti
// DATA_PATH può essere impostato nel docker-compose per usare un volume persistente
var dataBasePath = Environment.GetEnvironmentVariable("DATA_PATH");
if (string.IsNullOrEmpty(dataBasePath))
{
// Fallback: usa directory relativa all'applicazione
dataBasePath = Path.Combine(AppContext.BaseDirectory, "Data");
}
// Crea directory se non esiste
if (!Directory.Exists(dataBasePath))
{
Directory.CreateDirectory(dataBasePath);
}
Console.WriteLine($"[Startup] Data path: {dataBasePath}");
// Configura Data Protection per evitare CryptographicException
var dataProtectionPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AutoBidder",
"DataProtection-Keys"
);
var dataProtectionPath = Path.Combine(dataBasePath, "DataProtection-Keys");
if (!Directory.Exists(dataProtectionPath))
{
@@ -79,6 +106,57 @@ builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(dataProtectionPath))
.SetApplicationName("AutoBidder");
// Database per Identity (SQLite)
var identityDbPath = Path.Combine(dataBasePath, "identity.db");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlite($"Data Source={identityDbPath}");
});
// ASP.NET Core Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
// Password settings (SICUREZZA FORTE)
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequiredLength = 12;
options.Password.RequiredUniqueChars = 4;
// Lockout settings (protezione brute-force)
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// User settings
options.User.RequireUniqueEmail = false;
options.SignIn.RequireConfirmedAccount = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Cookie configuration (SICUREZZA TAILSCALE)
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = "AutoBidder.Auth";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // HTTP su Tailscale OK
options.Cookie.SameSite = SameSiteMode.Lax;
options.ExpireTimeSpan = TimeSpan.FromDays(7);
options.SlidingExpiration = true;
// Redirect per autenticazione (Razor Pages)
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
options.AccessDeniedPath = "/Account/Login";
});
// Authorization
builder.Services.AddAuthorization();
builder.Services.AddCascadingAuthenticationState();
// Configura HTTPS Redirection per produzione
if (!builder.Environment.IsDevelopment())
{
@@ -90,71 +168,6 @@ if (!builder.Environment.IsDevelopment())
});
}
// Configura Database SQLite per statistiche (fallback locale)
builder.Services.AddDbContext<StatisticsContext>(options =>
{
var dbPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AutoBidder",
"statistics.db"
);
// Crea directory se non esiste
var directory = Path.GetDirectoryName(dbPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
options.UseSqlite($"Data Source={dbPath}");
});
// Configura Database PostgreSQL per statistiche avanzate
var usePostgres = builder.Configuration.GetValue<bool>("Database:UsePostgres", false);
if (usePostgres)
{
try
{
var connString = builder.Environment.IsProduction()
? builder.Configuration.GetConnectionString("PostgresStatsProduction")
: builder.Configuration.GetConnectionString("PostgresStats");
// Sostituisci variabili ambiente in production
if (builder.Environment.IsProduction())
{
connString = connString?
.Replace("${POSTGRES_USER}", Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "autobidder")
.Replace("${POSTGRES_PASSWORD}", Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "");
}
if (!string.IsNullOrEmpty(connString))
{
builder.Services.AddDbContext<AutoBidder.Data.PostgresStatsContext>(options =>
{
options.UseNpgsql(connString, npgsqlOptions =>
{
npgsqlOptions.EnableRetryOnFailure(3);
npgsqlOptions.CommandTimeout(30);
});
});
Console.WriteLine("[Startup] PostgreSQL configured for statistics");
}
else
{
Console.WriteLine("[Startup] PostgreSQL connection string not found - using SQLite only");
}
}
catch (Exception ex)
{
Console.WriteLine($"[Startup] PostgreSQL configuration failed: {ex.Message} - using SQLite only");
}
}
else
{
Console.WriteLine("[Startup] PostgreSQL disabled in configuration - using SQLite only");
}
// Registra servizi applicazione come Singleton per condividere stato
var htmlCacheService = new HtmlCacheService(
maxConcurrentRequests: 3,
@@ -163,31 +176,18 @@ 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()));
builder.Services.AddSingleton<DatabaseService>();
builder.Services.AddSingleton<ApplicationStateService>();
builder.Services.AddScoped<StatsService>(sp =>
{
var db = sp.GetRequiredService<DatabaseService>();
// Prova a ottenere PostgreSQL context (potrebbe essere null)
AutoBidder.Data.PostgresStatsContext? postgresDb = null;
try
{
postgresDb = sp.GetService<AutoBidder.Data.PostgresStatsContext>();
}
catch
{
// PostgreSQL non disponibile, usa solo SQLite
}
return new StatsService(db, postgresDb);
});
builder.Services.AddSingleton<BidooBrowserService>();
builder.Services.AddScoped<StatsService>();
builder.Services.AddScoped<AuctionStateService>();
// Configura SignalR per real-time updates
@@ -199,6 +199,63 @@ builder.Services.AddSignalR(options =>
var app = builder.Build();
// ============================================
// INIZIALIZZAZIONE DATABASE IDENTITY
// ============================================
using (var scope = app.Services.CreateScope())
{
try
{
var identityDb = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
// Crea database Identity
await identityDb.Database.EnsureCreatedAsync();
Console.WriteLine("[Identity] Database initialized");
// Crea utente admin se non esiste
var adminUsername = Environment.GetEnvironmentVariable("ADMIN_USERNAME") ?? "admin";
var adminPassword = Environment.GetEnvironmentVariable("ADMIN_PASSWORD");
// Password di default se non configurata (stessa per debug e container)
if (string.IsNullOrEmpty(adminPassword))
{
adminPassword = "Admin@Password123!";
}
var existingAdmin = await userManager.FindByNameAsync(adminUsername);
if (existingAdmin == null)
{
var adminUser = new ApplicationUser
{
UserName = adminUsername,
Email = $"{adminUsername}@autobidder.local",
EmailConfirmed = true,
IsActive = true,
CreatedAt = DateTime.UtcNow
};
var result = await userManager.CreateAsync(adminUser, adminPassword);
if (result.Succeeded)
{
Console.WriteLine($"[Identity] Admin user created: {adminUsername}");
}
else
{
Console.WriteLine($"[Identity] Failed to create admin user: {string.Join(", ", result.Errors.Select(e => e.Description))}");
}
}
else
{
Console.WriteLine($"[Identity] Admin user exists: {adminUsername}");
}
}
catch (Exception ex)
{
Console.WriteLine($"[Identity] Initialization error: {ex.Message}");
}
}
// ??? NUOVO: Inizializza DatabaseService
using (var scope = app.Services.CreateScope())
{
@@ -221,139 +278,126 @@ using (var scope = app.Services.CreateScope())
// Verifica salute database
var isHealthy = await databaseService.CheckDatabaseHealthAsync();
Console.WriteLine($"[DB] Database health check: {(isHealthy ? "OK" : "FAILED")}");
// 🔥 MANUTENZIONE AUTOMATICA DATABASE
var settings = AutoBidder.Utilities.SettingsManager.Load();
if (settings.DatabaseAutoCleanupDuplicates)
{
Console.WriteLine("[DB] Checking for duplicate records...");
var duplicateCount = await databaseService.CountDuplicateAuctionResultsAsync();
if (duplicateCount > 0)
{
Console.WriteLine($"[DB] Found {duplicateCount} duplicates - removing...");
var removed = await databaseService.RemoveDuplicateAuctionResultsAsync();
Console.WriteLine($"[DB] ✓ Removed {removed} duplicate auction results");
}
else
{
Console.WriteLine("[DB] ✓ No duplicates found");
}
}
if (settings.DatabaseAutoCleanupIncomplete)
{
Console.WriteLine("[DB] Checking for incomplete records...");
var incompleteCount = await databaseService.CountIncompleteAuctionResultsAsync();
if (incompleteCount > 0)
{
Console.WriteLine($"[DB] Found {incompleteCount} incomplete records - removing...");
var removed = await databaseService.RemoveIncompleteAuctionResultsAsync();
Console.WriteLine($"[DB] ✓ Removed {removed} incomplete auction results");
}
else
{
Console.WriteLine("[DB] ✓ No incomplete records found");
}
}
if (settings.DatabaseMaxRetentionDays > 0)
{
Console.WriteLine($"[DB] Checking for records older than {settings.DatabaseMaxRetentionDays} days...");
var oldCount = await databaseService.RemoveOldAuctionResultsAsync(settings.DatabaseMaxRetentionDays);
if (oldCount > 0)
{
Console.WriteLine($"[DB] ✓ Removed {oldCount} old auction results");
}
else
{
Console.WriteLine($"[DB] ✓ No old records to remove");
}
}
// 🆕 Esegui diagnostica completa se ci sono problemi o se richiesto
var runDiagnostics = Environment.GetEnvironmentVariable("DB_DIAGNOSTICS")?.ToLower() == "true";
if (!isHealthy || runDiagnostics)
{
Console.WriteLine("[DB] Running full diagnostics...");
await databaseService.RunDatabaseDiagnosticsAsync();
}
}
catch (Exception ex)
{
Console.WriteLine($"[DB ERROR] Failed to initialize database: {ex.Message}");
Console.WriteLine($"[DB ERROR] Stack trace: {ex.StackTrace}");
}
}
// Crea database statistiche se non esiste (senza migrations)
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<StatisticsContext>();
try
{
// Log percorso database
var connection = db.Database.GetDbConnection();
Console.WriteLine($"[STATS DB] Database path: {connection.DataSource}");
// Verifica se database esiste
var dbExists = db.Database.CanConnect();
Console.WriteLine($"[STATS DB] Database exists: {dbExists}");
// Forza creazione tabelle se non esistono
if (!dbExists || !db.ProductStats.Any())
{
Console.WriteLine("[STATS DB] Creating database schema...");
db.Database.EnsureDeleted(); // Elimina database vecchio
db.Database.EnsureCreated(); // Ricrea con schema aggiornato
Console.WriteLine("[STATS DB] Database schema created successfully");
}
else
{
Console.WriteLine($"[STATS DB] Database already exists with {db.ProductStats.Count()} records");
}
}
catch (Exception ex)
{
Console.WriteLine($"[STATS DB ERROR] Failed to initialize database: {ex.Message}");
Console.WriteLine($"[STATS DB ERROR] Stack trace: {ex.StackTrace}");
// Prova a ricreare forzatamente
// In caso di errore, esegui sempre la diagnostica
try
{
Console.WriteLine("[STATS DB] Attempting forced recreation...");
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
Console.WriteLine("[STATS DB] Forced recreation successful");
await databaseService.RunDatabaseDiagnosticsAsync();
}
catch (Exception ex2)
catch
{
Console.WriteLine($"[STATS DB ERROR] Forced recreation failed: {ex2.Message}");
// Ignora errori nella diagnostica stessa
}
}
}
// Inizializza PostgreSQL per statistiche avanzate
using (var scope = app.Services.CreateScope())
// ?? NUOVO: Collega evento OnAuctionCompleted per salvare statistiche
{
try
var dbService = app.Services.GetRequiredService<DatabaseService>();
auctionMonitor.OnAuctionCompleted += async (auction, state, won) =>
{
var postgresDb = scope.ServiceProvider.GetService<AutoBidder.Data.PostgresStatsContext>();
if (postgresDb != null)
try
{
Console.WriteLine("[PostgreSQL] Initializing PostgreSQL statistics database...");
Console.WriteLine($"");
Console.WriteLine($"╔════════════════════════════════════════════════════════════════");
Console.WriteLine($"║ [EVENTO] Asta Terminata - Salvataggio Statistiche");
Console.WriteLine($"╠════════════════════════════════════════════════════════════════");
Console.WriteLine($"║ Asta: {auction.Name}");
Console.WriteLine($"║ ID: {auction.AuctionId}");
Console.WriteLine($"║ Stato: {(won ? " VINTA" : " PERSA")}");
Console.WriteLine($"╚════════════════════════════════════════════════════════════════");
Console.WriteLine($"");
var autoCreateSchema = app.Configuration.GetValue<bool>("Database:AutoCreateSchema", true);
// Crea un nuovo scope per StatsService (è Scoped)
using var scope = app.Services.CreateScope();
var statsService = scope.ServiceProvider.GetRequiredService<StatsService>();
if (autoCreateSchema)
{
// Usa il metodo EnsureSchemaAsync che gestisce la creazione automatica
var schemaCreated = await postgresDb.EnsureSchemaAsync();
if (schemaCreated)
{
// Valida che tutte le tabelle siano state create
var schemaValid = await postgresDb.ValidateSchemaAsync();
if (schemaValid)
{
Console.WriteLine("[PostgreSQL] Statistics features ENABLED");
}
else
{
Console.WriteLine("[PostgreSQL] Schema validation failed");
Console.WriteLine("[PostgreSQL] Statistics features DISABLED (missing tables)");
}
}
else
{
Console.WriteLine("[PostgreSQL] Cannot connect to database");
Console.WriteLine("[PostgreSQL] Statistics features will use SQLite fallback");
}
}
else
{
Console.WriteLine("[PostgreSQL] Auto-create schema disabled");
// Prova comunque a validare lo schema esistente
try
{
var schemaValid = await postgresDb.ValidateSchemaAsync();
if (schemaValid)
{
Console.WriteLine("[PostgreSQL] Existing schema validated successfully");
Console.WriteLine("[PostgreSQL] Statistics features ENABLED");
}
else
{
Console.WriteLine("[PostgreSQL] Statistics features DISABLED (schema not found)");
}
}
catch
{
Console.WriteLine("[PostgreSQL] Statistics features DISABLED (schema not found)");
}
}
await statsService.RecordAuctionCompletedAsync(auction, state, won);
// ✅ CORRETTO: Log di successo SOLO se non ci sono eccezioni
Console.WriteLine($"");
Console.WriteLine($"[EVENTO] ✓ Asta salvata con successo nel database");
Console.WriteLine($"");
}
else
catch (Exception ex)
{
Console.WriteLine("[PostgreSQL] Not configured - Statistics will use SQLite only");
Console.WriteLine($"");
Console.WriteLine($"[EVENTO ERROR] ✗ Errore durante salvataggio statistiche:");
Console.WriteLine($"[EVENTO ERROR] {ex.Message}");
Console.WriteLine($"[EVENTO ERROR] Stack: {ex.StackTrace}");
Console.WriteLine($"");
}
}
catch (Exception ex)
{
Console.WriteLine($"[PostgreSQL ERROR] Initialization failed: {ex.Message}");
Console.WriteLine($"[PostgreSQL ERROR] Stack trace: {ex.StackTrace}");
Console.WriteLine($"[PostgreSQL] Statistics features will use SQLite fallback");
}
};
Console.WriteLine("[STARTUP] OnAuctionCompleted event handler registered");
}
// ??? NUOVO: Ripristina aste salvate e riprendi monitoraggio se configurato
// ? NUOVO: Ripristina aste salvate e riprendi monitoraggio se configurato
using (var scope = app.Services.CreateScope())
{
try
@@ -388,15 +432,26 @@ using (var scope = app.Services.CreateScope())
// Gestisci comportamento di avvio
if (settings.RememberAuctionStates)
{
// Modalità "Ricorda Stato": mantiene lo stato salvato di ogni asta
var activeAuctions = savedAuctions.Where(a => a.IsActive && !a.IsPaused).ToList();
// Modalità "Ricorda Stato": mantiene lo stato salvato di ogni asta
// 🔥 FIX CRITICO: Avvia monitor anche per aste in pausa (IsActive=true)
var activeAuctions = savedAuctions.Where(a => a.IsActive).ToList();
var resumeAuctions = savedAuctions.Where(a => a.IsActive && !a.IsPaused).ToList();
var pausedAuctions = savedAuctions.Where(a => a.IsActive && a.IsPaused).ToList();
if (activeAuctions.Any())
{
Console.WriteLine($"[STARTUP] Resuming monitoring for {activeAuctions.Count} active auctions");
Console.WriteLine($"[STARTUP] Starting monitor for {activeAuctions.Count} active auctions ({resumeAuctions.Count} active, {pausedAuctions.Count} paused)");
monitor.Start();
appState.IsMonitoringActive = true;
appState.AddLog($"[STARTUP] Ripristinato stato salvato: {activeAuctions.Count} aste attive");
if (pausedAuctions.Any())
{
appState.AddLog($"[STARTUP] Ripristinate {resumeAuctions.Count} aste attive + {pausedAuctions.Count} in pausa (polling attivo)");
}
else
{
appState.AddLog($"[STARTUP] Ripristinato stato salvato: {resumeAuctions.Count} aste attive");
}
}
else
{
@@ -406,7 +461,7 @@ using (var scope = app.Services.CreateScope())
}
else
{
// Modalità "Default": applica DefaultStartAuctionsOnLoad a tutte le aste
// Modalità "Default": applica DefaultStartAuctionsOnLoad a tutte le aste
switch (settings.DefaultStartAuctionsOnLoad)
{
case "Active":
@@ -466,7 +521,7 @@ if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// Abilita HSTS solo se HTTPS è attivo
// Abilita HSTS solo se HTTPS è attivo
if (enableHttps)
{
app.UseHsts();
@@ -477,7 +532,7 @@ else
app.UseDeveloperExceptionPage();
}
// Abilita HTTPS redirection solo se HTTPS è configurato
// Abilita HTTPS redirection solo se HTTPS è configurato
if (enableHttps)
{
app.UseHttpsRedirection();
@@ -486,7 +541,58 @@ if (enableHttps)
app.UseStaticFiles();
app.UseRouting();
// ============================================
// MIDDLEWARE AUTENTICAZIONE E AUTORIZZAZIONE
// ============================================
app.UseAuthentication();
app.UseAuthorization();
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();
+53 -22
View File
@@ -1,70 +1,101 @@
# ?? AutoBidder - Sistema Automatizzato Gestione Aste Bidoo
[![Version](https://img.shields.io/badge/version-1.1.0-blue.svg)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-1.2.0-blue.svg)](CHANGELOG.md)
[![.NET](https://img.shields.io/badge/.NET-8.0-purple.svg)](https://dotnet.microsoft.com/)
[![Blazor](https://img.shields.io/badge/Blazor-Server-orange.svg)](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor)
[![Docker](https://img.shields.io/badge/Docker-Ready-brightgreen.svg)](Dockerfile)
[![Security](https://img.shields.io/badge/Security-Identity-green.svg)](SECURITY.md)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
Sistema Blazor .NET 8 per il monitoraggio e la partecipazione automatica alle aste Bidoo.
Sistema Blazor .NET 8 per il monitoraggio e la partecipazione automatica alle aste Bidoo, con **autenticazione sicura** per deploy Tailscale.
---
## ?? Quick Start
### ?? NUOVO v1.2.0: Configurazione Sicurezza
```bash
# 1. Copia e configura credenziali
cp .env.example .env
nano .env # Imposta ADMIN_PASSWORD
# 2. Avvia container
docker-compose up -d
# 3. Primo login
# Browser: http://localhost:5000/login
# Username: admin
# Password: (valore ADMIN_PASSWORD)
```
### Docker (CONSIGLIATO)
```bash
# Pull ultima versione da Gitea
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.2.0
# Avvia container
# Avvia container CON AUTENTICAZIONE
docker run -d \
--name autobidder \
-p 5000:8080 \
-e ADMIN_USERNAME=admin \
-e ADMIN_PASSWORD="TuaPasswordSicura123!" \
-v /path/to/data:/app/Data \
gitea.encke-hake.ts.net/alby96/autobidder:latest
gitea.encke-hake.ts.net/alby96/autobidder:1.2.0
# Accedi a http://localhost:5000
# Accedi a http://localhost:5000/login
```
### Docker Compose
```bash
# 1. Configura .env
cp .env.example .env
# Imposta ADMIN_PASSWORD in .env
# 2. Avvia stack
docker-compose up -d
```
### Development Locale
```bash
# Imposta password admin
export ADMIN_PASSWORD="DevPassword123!"
# Avvia applicazione
dotnet run --project AutoBidder.csproj
# Accedi a https://localhost:5001
# Accedi a http://localhost:8080/login
```
---
## ?? Versione Corrente: `1.1.0`
## ?? Versione Corrente: `1.2.0`
**Release:** 2025-01-18
**Tipo:** MINOR (nuove feature + bug fix)
**Tipo:** MINOR (feature sicurezza + autenticazione)
### Novità v1.1.0
### ?? Novità v1.2.0 - SICUREZZA
- ? **Pubblicazione automatica Gitea Container Registry**
- Workflow integrato Visual Studio
- Versionamento automatico
- Tag multipli (latest + versione)
- ?? **Sistema autenticazione completo**
- Login username/password con ASP.NET Core Identity
- Protezione brute-force (lockout 15 min dopo 5 tentativi)
- Cookie sicuri (HttpOnly, SameSite)
- Password policy forte (min 12 caratteri)
- ?? **Configurazione Docker migliorata**
- HTTPS disabilitato di default (gestito da reverse proxy)
- Porta HTTP standardizzata (8080)
- Convenzione path Gitea corretta
- ??? **Protezione route**
- Tutte le pagine richiedono autenticazione
- Redirect automatico a `/login`
- Gestione sessioni sicura
- ?? **Fix critici**
- Risolto errore Visual Studio "ContainerBuild"
- Risolto crash container per certificati HTTPS
- ?? **Configurazione utente admin**
- Username/password via environment variables
- Password temporanea se non configurata (?? da cambiare!)
- Database Identity SQLite persistente
**[?? Changelog Completo](CHANGELOG.md)** | **[?? Guida Migrazione](CHANGELOG.md#note-di-migrazione)**
**[?? Changelog Completo](CHANGELOG.md)** | **[?? Guida Sicurezza](SECURITY.md)**
---
-280
View File
@@ -1,280 +0,0 @@
# ? RELEASE v1.1.1 - Fix Porta Container
## ?? Bug Fix Critico
**Versione:** `1.1.0` ? **`1.1.1`**
**Tipo:** PATCH (bug fix)
**Data:** 2025-01-18
---
## ? Problema Riscontrato
### Sintomi
- ? Container si avvia senza errori
- ? Log mostra "Application started"
- ? Pagina web non carica
- ? Browser timeout o "connection refused"
### Diagnosi
**Log container:**
```
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://[::]:5000 ? SBAGLIATO!
```
**Configurazione attesa:**
```dockerfile
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
```
**Port mapping:**
```yaml
ports:
- "5000:8080" # Host ? Container
```
**Problema:** Container ascolta su 5000, ma port mapping cerca 8080 ? **MISMATCH!**
---
## ? Soluzione Applicata
### Modifica `Program.cs`
**Prima (ERRATO):**
```csharp
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(8080); // ? Ignorato!
// ...
});
```
**Dopo (CORRETTO):**
```csharp
// NO configurazione esplicita HTTP
// ASPNETCORE_URLS gestisce tutto
if (enableHttps)
{
// Solo configurazione HTTPS opzionale
builder.WebHost.ConfigureKestrel(options =>
{
// Porta 8443 per HTTPS
});
}
else
{
// Log porta HTTP da ASPNETCORE_URLS
Console.WriteLine($"[Kestrel] Listening on: {ASPNETCORE_URLS}");
}
```
### Risultato
**Log corretto:**
```
[Kestrel] HTTPS disabled - running in HTTP-only mode
[Kestrel] Listening on: http://+:8080
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://[::]:8080 ? CORRETTO!
```
---
## ?? Come Testare
### 1. Rebuild Container
```bash
# Stop container vecchio
docker stop autobidder
docker rm autobidder
# Pull versione 1.1.1
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.1.1
# Oppure build locale
docker build -t autobidder:1.1.1 .
```
### 2. Avvia Container
```bash
docker run -d \
--name autobidder \
-p 5000:8080 \
-v /data:/app/Data \
gitea.encke-hake.ts.net/alby96/autobidder:1.1.1
```
### 3. Verifica Log
```bash
docker logs autobidder | grep "Now listening"
# Output atteso:
# Now listening on: http://[::]:8080 ?
```
### 4. Test Accesso
```bash
# Apri browser
http://localhost:5000
# Dovrebbe caricare la homepage AutoBidder ?
```
---
## ?? File Modificati
| File | Modifica | Motivo |
|------|----------|--------|
| `Program.cs` | Rimossa configurazione esplicita porta HTTP | Fix conflitto Kestrel |
| `AutoBidder.csproj` | Versione `1.1.1` | Incremento PATCH |
| `Dockerfile` | Label version `1.1.1` | Metadata immagine |
| `CHANGELOG.md` | Entry v1.1.1 | Documentazione fix |
| `FIX_PORTA_CONTAINER.md` | Nuovo documento | Troubleshooting dettagliato |
---
## ?? Migrazione da v1.1.0
**Nessuna breaking change!**
Aggiornamento semplice:
```bash
# Docker
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.1.1
docker-compose up -d
# Unraid
# Cambia tag immagine: latest ? 1.1.1
# Restart container
```
---
## ?? Documentazione
### Nuovi Documenti
- **`FIX_PORTA_CONTAINER.md`** - Troubleshooting dettagliato problema porta
- Diagnosi completa
- Soluzione passo-passo
- Test e verifica
- Override porta avanzato
### Documenti Aggiornati
- `CHANGELOG.md` - Entry v1.1.1
- `README.md` - Badge versione aggiornato
---
## ?? Benefici Fix
### Prima (v1.1.0)
- ? Container parte ma pagina non carica
- ? Port mismatch difficile da diagnosticare
- ? Configurazione confusa
- ? Conflitti Kestrel vs ASPNETCORE_URLS
### Dopo (v1.1.1)
- ? Container accessibile immediatamente
- ? Porta configurata centralmente (ASPNETCORE_URLS)
- ? Log chiaro della porta in ascolto
- ? Nessun conflitto configurazione
- ? Più facile override porta
---
## ? Checklist Completata
- [x] Problema identificato (porta 5000 vs 8080)
- [x] Root cause trovata (conflitto configurazione)
- [x] Fix applicato (rimossa config esplicita)
- [x] Build testata
- [x] Versione incrementata (1.1.1)
- [x] CHANGELOG aggiornato
- [x] Documentazione creata
- [x] Immagine pronta per pubblicazione
---
## ?? Prossimi Passi
### Pubblica su Gitea
```bash
# Da Visual Studio
# Tasto destro ? Pubblica ? GiteaRegistry
# Oppure CLI
dotnet publish /p:PublishProfile=GiteaRegistry
```
### Commit e Tag
```bash
git add .
git commit -m "fix: container listening on wrong port (5000 instead of 8080)
- Remove explicit HTTP configuration from Kestrel
- Let ASPNETCORE_URLS control HTTP port
- Kestrel config now only for optional HTTPS
- Fixes web page not loading when accessing container
Resolves #XX"
git tag v1.1.1
git push origin docker --tags
```
---
## ?? Metriche Fix
- **Tempo diagnosi:** ~10 minuti
- **Tempo fix:** ~5 minuti
- **Righe modificate:** ~30 righe
- **File modificati:** 5 file
- **Documentazione:** 1 nuovo doc + aggiornamenti
- **Impatto:** **CRITICO** (container inaccessibile)
- **Difficoltà:** **BASSA** (una volta identificato)
---
## ?? Lezioni Apprese
1. **Configurazione esplicita vs variabili ambiente**
- Configurazione esplicita ha precedenza
- Può causare conflitti difficili da debuggare
- Meglio centralizzare config in env vars
2. **Verifica sempre i log**
- "Now listening on:" mostra porta EFFETTIVA
- Può essere diversa da quella configurata
- Non fidarsi solo della configurazione
3. **Port mapping deve corrispondere**
- Verifica porta container vs port mapping
- Usa `docker port <container>` per verificare
- Test endpoint prima di troubleshooting complesso
4. **Keep It Simple**
- Meno configurazione = meno problemi
- ASPNETCORE_URLS è il modo standard
- ConfigureKestrel solo per casi speciali
---
**? v1.1.1 PRONTO - Fix Critico Applicato!**
Container ora accessibile correttamente sulla porta 8080! ??
-289
View File
@@ -1,289 +0,0 @@
# ? RIEPILOGO COMPLETO - CONFIGURAZIONE DOCKER + GITEA
## ?? Problemi Risolti
### 1. ? Convenzione Nomi Registry Gitea
**Problema:** Path errato con 4 livelli invece di 3
- ? Prima: `gitea.../alby96/mimante/autobidder`
- ? Dopo: `gitea.../alby96/autobidder`
### 2. ? Errore Visual Studio "ContainerBuild"
**Problema:** Profilo usava `WebPublishMethod=Docker` senza SDK
- ? Prima: Richiede Microsoft.Docker.Sdk
- ? Dopo: `WebPublishMethod=Custom` senza dipendenze
### 3. ? Container HTTPS Crash
**Problema:** Kestrel cerca certificati HTTPS inesistenti
- ? Prima: HTTPS abilitato di default, crash all'avvio
- ? Dopo: HTTP only (8080), HTTPS opzionale
---
## ?? File Modificati
| File | Modifica | Motivo |
|------|----------|--------|
| `AutoBidder.csproj` | `<ContainerRegistry>` corretto | Convenzione Gitea 3 livelli |
| `AutoBidder.csproj` | Post-build target aggiunto | Push automatico su Gitea |
| `GiteaRegistry.pubxml` | `WebPublishMethod=Custom` | Nessuna dipendenza SDK Docker |
| `GiteaRegistry.pubxml` | Target `DockerBuild` | Build Docker integrato |
| `Program.cs` | `enableHttps=false` default | HTTPS disabilitato in container |
| `Program.cs` | Porta `8080` | Standard container HTTP |
| `Dockerfile` | `ENV Kestrel__EnableHttps=false` | Conferma HTTP only |
| `Dockerfile` | `EXPOSE 8080` | Porta HTTP standard |
| `docker-compose.yml` | `5000:8080` port mapping | Host:Container corretto |
---
## ?? Workflow Finale
### Da Visual Studio (1 Click)
```
1. Tasto destro progetto ? Pubblica ? GiteaRegistry
2. Visual Studio:
?? Build .NET (Release)
?? Target DockerBuild (profilo) ? docker build
?? Post-build Gitea (csproj) ? tag + push
?? ? SUCCESS!
```
### Output Completo
```
?????????????????????????????????????????????????????????????????????
? DOCKER BUILD: Building container image ?
?????????????????????????????????????????????????????????????????????
?? Building: autobidder:latest
? Docker build completed successfully!
?????????????????????????????????????????????????????????????????????
? POST-BUILD: Pubblicazione su Gitea Container Registry ?
?????????????????????????????????????????????????????????????????????
?? Solution Version: 1.0.0
??? Target Tags:
• gitea.encke-hake.ts.net/alby96/autobidder:latest
• gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:latest
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:latest
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
?????????????????????????????????????????????????????????????????????
? ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ?
?????????????????????????????????????????????????????????????????????
```
---
## ?? Configurazione Container
### Porte
| Ambiente | Host | Container | Protocollo |
|----------|------|-----------|------------|
| **Development** | 5001 | 5001 | HTTPS (dev cert) |
| **Docker/Production** | 5000 | 8080 | HTTP |
| **HTTPS Production** | 443 | 8443 | HTTPS (con cert) |
### Variabili Ambiente
```bash
# Container standard (HTTP only)
ASPNETCORE_URLS=http://+:8080
ASPNETCORE_ENVIRONMENT=Production
Kestrel__EnableHttps=false
# Con HTTPS (opzionale, richiede certificato)
Kestrel__EnableHttps=true
Kestrel__Certificates__Default__Path=/certs/cert.pfx
Kestrel__Certificates__Default__Password=password
```
---
## ?? Deploy su Gitea
### Immagini Pubblicate
```
gitea.encke-hake.ts.net/alby96/autobidder:latest
gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
```
**Link Gitea:**
```
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder
```
### Versionamento Automatico
```xml
<!-- AutoBidder.csproj -->
<Version>1.0.1</Version> ? Incrementa qui
```
Pubblica ? Crea automaticamente:
- Tag `latest` (aggiornato)
- Tag `1.0.1` (nuovo)
---
## ?? Comandi Rapidi
### Autenticazione Gitea
```bash
docker login gitea.encke-hake.ts.net
# Username: Alby96
# Password: [TOKEN PAT]
```
### Build Locale + Test
```bash
# Build
docker build -t autobidder:test .
# Test locale
docker run -p 5000:8080 \
-v $(pwd)/Data:/app/Data \
autobidder:test
# Apri: http://localhost:5000
```
### Pull da Gitea
```bash
# Latest
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
# Versione specifica (production)
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
```
### Deploy Production
```bash
docker run -d \
--name autobidder \
-p 5000:8080 \
-v /data/autobidder:/app/Data \
-v /logs/autobidder:/app/logs \
-e ASPNETCORE_ENVIRONMENT=Production \
--restart unless-stopped \
gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
```
### Docker Compose
```bash
# Start
docker-compose up -d
# Logs
docker-compose logs -f autobidder
# Stop
docker-compose down
# Rebuild
docker-compose up -d --build
```
---
## ?? Reverse Proxy (HTTPS in Production)
### Nginx
```nginx
server {
listen 443 ssl http2;
server_name autobidder.example.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
location / {
proxy_pass http://autobidder:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
### Traefik
```yaml
services:
autobidder:
image: gitea.encke-hake.ts.net/alby96/autobidder:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.autobidder.rule=Host(`autobidder.example.com`)"
- "traefik.http.routers.autobidder.tls=true"
- "traefik.http.routers.autobidder.tls.certresolver=letsencrypt"
- "traefik.http.services.autobidder.loadbalancer.server.port=8080"
```
---
## ?? Checklist Finale
### Configurazione
- [x] Convenzione Gitea corretta (3 livelli)
- [x] Versionamento automatico da `.csproj`
- [x] HTTPS disabilitato in container
- [x] Porta HTTP 8080 (standard)
- [x] Post-build push automatico
- [x] Profilo Visual Studio senza errori
### Pubblicazione
- [x] Build locale funziona
- [x] Docker build funziona
- [x] Tag Gitea creati (`latest` + versione)
- [x] Push su Gitea riuscito
- [x] Immagini visibili su Gitea
- [x] Visual Studio SUCCESS
### Container
- [x] Container si avvia senza errori
- [x] HTTP accessibile su porta 8080
- [x] Volumi persistenti configurati
- [x] Healthcheck funzionante
- [x] Logs visibili
### Documentazione
- [x] DOCKER_PUBLISH_GUIDE.md completa
- [x] PROBLEMA_RISOLTO.md (Visual Studio)
- [x] PROBLEMA_HTTPS_RISOLTO.md (Container)
- [x] CONFIGURAZIONE_FINALE.md
- [x] NUOVO_WORKFLOW_RIEPILOGO.md
- [x] Questo riepilogo
---
## ?? STATO: TUTTO FUNZIONANTE!
**Workflow completo e testato:**
1. ? Modifica codice
2. ? Incrementa versione in `.csproj`
3. ? Pubblica da Visual Studio (1 click)
4. ? Immagini su Gitea (latest + versione)
5. ? Deploy su Unraid/Docker
**Nessun errore, tutto automatico, versionamento tracciato!** ??
-376
View File
@@ -1,376 +0,0 @@
# ?? RIEPILOGO FINALE - RELEASE v1.1.0
## ? Lavoro Completato
### ?? Versione Rilasciata
**Versione:** `1.1.0` (da `1.0.0`)
**Tipo:** MINOR (nuove feature + bug fix)
**Data:** 2025-01-18
---
## ?? File Creati (13 nuovi)
### Documentazione
1. **`README.md`** - Homepage progetto con badge e quick start
2. **`CHANGELOG.md`** - Storico completo modifiche (format standard)
3. **`VERSIONING.md`** - Guida sistema versionamento
4. **`VERSIONING_IMPLEMENTATO.md`** - Riepilogo implementazione
5. **`DOCKER_PUBLISH_GUIDE.md`** - Guida pubblicazione Gitea
6. **`CONFIGURAZIONE_FINALE.md`** - Riepilogo configurazione
7. **`NUOVO_WORKFLOW_RIEPILOGO.md`** - Dettagli workflow
8. **`VERIFICA_CONFIGURAZIONE_GITEA.md`** - Checklist conformità
9. **`PROBLEMA_RISOLTO.md`** - Fix errore Visual Studio
10. **`PROBLEMA_HTTPS_RISOLTO.md`** - Fix crash container
11. **`RIEPILOGO_COMPLETO_FINALE.md`** - Overview tutti i problemi
### Profili e Script
12. **`Properties/PublishProfiles/GiteaRegistry.pubxml`** - Profilo pubblicazione Gitea
13. **`bump-version.ps1`** - Script PowerShell per incremento versione automatico
---
## ?? File Modificati (4)
1. **`AutoBidder.csproj`**
- Versione aggiornata a `1.1.0`
- Post-build target per push Gitea
- Convenzione registry corretta
2. **`Program.cs`**
- HTTPS disabilitato di default
- Porta HTTP: `8080`
- Gestione certificati migliorata
3. **`Dockerfile`**
- Versione label aggiornata
- `ENV Kestrel__EnableHttps=false`
- Source URL corretto
4. **`docker-compose.yml`**
- Port mapping aggiornato `5000:8080`
- Convenzione registry corretta
---
## ? Funzionalità Implementate
### 1. ?? Pubblicazione Automatica su Gitea
**Workflow completo Visual Studio:**
```
Tasto destro ? Pubblica ? GiteaRegistry
?
Build .NET (Release)
?
Docker build (autobidder:latest)
?
Tag Gitea (latest + versione)
?
Push automatico
?
? SUCCESS!
```
**Output:**
- `gitea.encke-hake.ts.net/alby96/autobidder:latest`
- `gitea.encke-hake.ts.net/alby96/autobidder:1.1.0`
### 2. ?? Sistema Versionamento Automatico
**Semantic Versioning implementato:**
- MAJOR: Breaking changes (`1.x.x` ? `2.0.0`)
- MINOR: Nuove feature (`1.0.x` ? `1.1.0`)
- PATCH: Bug fix (`1.0.0` ? `1.0.1`)
**Strumenti:**
- `bump-version.ps1` - Script automatico incremento
- `CHANGELOG.md` - Storico modifiche
- `VERSIONING.md` - Guida completa
### 3. ?? Fix Container HTTPS
**Problema:**
```
System.InvalidOperationException: Unable to configure HTTPS endpoint
```
**Soluzione:**
- HTTPS disabilitato di default (`Kestrel__EnableHttps=false`)
- Porta HTTP standard: `8080`
- SSL gestito da reverse proxy
### 4. ?? Fix Visual Studio
**Problema:**
```
Errore MSB4057: target "ContainerBuild" non presente
```
**Soluzione:**
- Profilo `Custom` senza dipendenze Docker SDK
- Target `DockerBuild` integrato
- Workflow senza errori
### 5. ? Convenzione Gitea Corretta
**Prima (ERRATO):**
```
gitea.../alby96/mimante/autobidder (4 livelli)
```
**Dopo (CORRETTO):**
```
gitea.../alby96/autobidder (3 livelli)
```
---
## ?? Modifiche Breaking
### 1. Porta Container
**Prima:**
```bash
docker run -p 5000:5000 ...
```
**Dopo:**
```bash
docker run -p 5000:8080 ...
```
### 2. HTTPS
**Prima:**
- HTTPS abilitato di default
- Richiede certificati
**Dopo:**
- HTTP only di default
- HTTPS opzionale con certificato
### 3. Path Gitea
**Prima:**
```
gitea.../alby96/mimante/autobidder:latest
```
**Dopo:**
```
gitea.../alby96/autobidder:latest
```
---
## ?? Come Usare
### Incremento Versione Automatico
```powershell
# Bug fix
.\bump-version.ps1 -Type patch # 1.1.0 ? 1.1.1
# Nuova feature
.\bump-version.ps1 -Type minor # 1.1.0 ? 1.2.0
# Breaking change
.\bump-version.ps1 -Type major # 1.1.0 ? 2.0.0
```
### Pubblicazione su Gitea
**Da Visual Studio:**
1. Tasto destro progetto ? **Pubblica**
2. Seleziona: **`GiteaRegistry`**
3. Click **Pubblica**
**Da CLI:**
```bash
dotnet publish /p:PublishProfile=GiteaRegistry
```
### Deploy Production
```bash
# Pull versione specifica
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.1.0
# Avvia container
docker run -d \
--name autobidder \
-p 5000:8080 \
-v /data:/app/Data \
gitea.encke-hake.ts.net/alby96/autobidder:1.1.0
```
---
## ?? Documentazione Disponibile
### Guide Utente
| Documento | Scopo |
|-----------|-------|
| `README.md` | Homepage progetto, quick start, overview |
| `CHANGELOG.md` | Storico modifiche per versione |
| `DOCKER_PUBLISH_GUIDE.md` | Guida pubblicazione Gitea step-by-step |
### Guide Sviluppatore
| Documento | Scopo |
|-----------|-------|
| `VERSIONING.md` | Sistema versionamento, workflow release |
| `CONFIGURAZIONE_FINALE.md` | Riepilogo configurazione Docker/Gitea |
| `NUOVO_WORKFLOW_RIEPILOGO.md` | Dettagli tecnici workflow pubblicazione |
### Troubleshooting
| Documento | Scopo |
|-----------|-------|
| `PROBLEMA_RISOLTO.md` | Fix errore Visual Studio |
| `PROBLEMA_HTTPS_RISOLTO.md` | Fix crash container HTTPS |
| `VERIFICA_CONFIGURAZIONE_GITEA.md` | Checklist conformità |
### Riepilogo
| Documento | Scopo |
|-----------|-------|
| `RIEPILOGO_COMPLETO_FINALE.md` | Overview completa tutti i problemi |
| `VERSIONING_IMPLEMENTATO.md` | Dettagli implementazione versioning |
| **`RIEPILOGO_RELEASE_v1.1.0.md`** | **Questo documento** |
---
## ? Checklist Completata
### Configurazione
- [x] Convenzione Gitea corretta (3 livelli)
- [x] Versionamento automatico da `.csproj`
- [x] HTTPS disabilitato in container
- [x] Porta HTTP 8080 (standard)
- [x] Post-build push automatico
- [x] Profilo Visual Studio funzionante
### Pubblicazione
- [x] Build locale testata
- [x] Docker build testato
- [x] Tag Gitea creati (`latest` + `1.1.0`)
- [x] Push su Gitea riuscito
- [x] Immagini visibili su Gitea
- [x] Visual Studio SUCCESS
### Container
- [x] Container si avvia senza errori
- [x] HTTP accessibile su porta 8080
- [x] Volumi persistenti configurati
- [x] Healthcheck funzionante
- [x] Logs visibili
### Documentazione
- [x] README.md completo
- [x] CHANGELOG.md con v1.1.0
- [x] VERSIONING.md con guida
- [x] Guide troubleshooting complete
- [x] Script automazione versione
- [x] Tutti i documenti aggiornati
---
## ?? Prossimi Passi
### Immediati
1. **Commit modifiche:**
```bash
git add .
git commit -m "chore: release v1.1.0
- Feature: Gitea publishing workflow
- Feature: Automatic versioning system
- Fix: Visual Studio ContainerBuild error
- Fix: Container HTTPS crash
- Docs: Complete documentation suite"
```
2. **Tag release:**
```bash
git tag v1.1.0
git push origin docker --tags
```
3. **Verifica pubblicazione:**
```
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder
```
### Futuro (v1.2.0)
- [ ] Notifiche email per aste vinte
- [ ] Export statistiche CSV/Excel
- [ ] Dashboard mobile-responsive
- [ ] API REST pubblica
---
## ?? Metriche Release
### File
- **Nuovi:** 13 file documentazione/script
- **Modificati:** 4 file sorgente
- **Righe totali:** ~3500+ righe documentazione
### Problemi Risolti
- ? Errore Visual Studio "ContainerBuild"
- ? Crash container certificati HTTPS
- ? Convenzione path Gitea errata
- ? Mancanza sistema versionamento
- ? Workflow pubblicazione manuale
### Funzionalità Aggiunte
- ? Pubblicazione automatica Gitea
- ? Versionamento semantico
- ? Script automazione versione
- ? Documentazione completa
---
## ?? STATO FINALE
```
?????????????????????????????????????????????????????????????????????
? ?
? ? RELEASE v1.1.0 COMPLETATA CON SUCCESSO! ?
? ?
? • Sistema versionamento implementato ?
? • Workflow Gitea automatizzato ?
? • Container HTTPS fix applicato ?
? • Visual Studio funzionante ?
? • Documentazione completa ?
? ?
? ?? Immagini disponibili su: ?
? gitea.encke-hake.ts.net/alby96/autobidder:latest ?
? gitea.encke-hake.ts.net/alby96/autobidder:1.1.0 ?
? ?
?????????????????????????????????????????????????????????????????????
```
**?? Sistema pronto per production deployment!**
---
**Data completamento:** 2025-01-18
**Versione:** 1.1.0
**Tipo release:** MINOR (feature + bugfix)
**Stato:** ? PRODUCTION READY
+183 -5
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
+544
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; }
}
}
+742
View File
@@ -0,0 +1,742 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using AutoBidder.Models;
namespace AutoBidder.Services
{
/// <summary>
/// Servizio per navigare le aste pubbliche di Bidoo senza autenticazione
/// Permette di esplorare le categorie e visualizzare le aste disponibili
/// </summary>
public class BidooBrowserService
{
private readonly HttpClient _httpClient;
private readonly List<BidooCategoryInfo> _cachedCategories = new();
private DateTime _categoriesCachedAt = DateTime.MinValue;
private readonly TimeSpan _categoryCacheExpiry = TimeSpan.FromMinutes(30);
public BidooBrowserService()
{
var handler = new HttpClientHandler
{
UseCookies = false,
AutomaticDecompression = System.Net.DecompressionMethods.All
};
_httpClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(15)
};
}
/// <summary>
/// Aggiunge headers browser-like per evitare blocchi
/// </summary>
private void AddBrowserHeaders(HttpRequestMessage request, string? referer = null)
{
request.Headers.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36");
request.Headers.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8");
request.Headers.Add("Accept-Language", "it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7");
request.Headers.Add("Accept-Encoding", "gzip, deflate, br");
request.Headers.Add("Cache-Control", "no-cache");
request.Headers.Add("Pragma", "no-cache");
if (!string.IsNullOrEmpty(referer))
{
request.Headers.Add("Referer", referer);
}
}
/// <summary>
/// Ottiene la lista delle categorie disponibili (con cache)
/// </summary>
public async Task<List<BidooCategoryInfo>> GetCategoriesAsync(bool forceRefresh = false, CancellationToken cancellationToken = default)
{
// Controlla cache
if (!forceRefresh && _cachedCategories.Count > 0 && DateTime.UtcNow - _categoriesCachedAt < _categoryCacheExpiry)
{
return _cachedCategories.ToList();
}
var categories = new List<BidooCategoryInfo>();
try
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://it.bidoo.com/");
AddBrowserHeaders(request);
var response = await _httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
var html = await response.Content.ReadAsStringAsync(cancellationToken);
// Aggiungi categorie speciali prima
categories.Add(new BidooCategoryInfo { TabId = 3, TagId = 0, DisplayName = "Tutte le aste", Slug = "", IsSpecialCategory = true, Icon = "bi-grid-3x3-gap" });
categories.Add(new BidooCategoryInfo { TabId = 1, TagId = 0, DisplayName = "Aste di Puntate", Slug = "", IsSpecialCategory = true, Icon = "bi-coin" });
categories.Add(new BidooCategoryInfo { TabId = 5, TagId = 0, DisplayName = "Aste Manuali", Slug = "", IsSpecialCategory = true, Icon = "bi-hand-index" });
// Parse categorie dal CategoryMenu
// Pattern: javascript:selectBids(4, true, false, 6); con data-tag="6" e testo "Buoni"
var categoryPattern = new Regex(
@"<a\s+href=""\s*javascript:selectBids\(4,\s*true,\s*false,\s*(\d+)\);\s*""\s+data-tab=""4""\s+data-slug=""([^""]*)""\s+data-tag=""(\d+)""><span[^>]*>([^<]+)</span></a>",
RegexOptions.IgnoreCase | RegexOptions.Singleline);
var matches = categoryPattern.Matches(html);
foreach (Match match in matches)
{
if (match.Success && match.Groups.Count >= 5)
{
int.TryParse(match.Groups[1].Value, out int tagId1);
var slug = match.Groups[2].Value.Trim();
int.TryParse(match.Groups[3].Value, out int tagId2);
var name = match.Groups[4].Value.Trim();
// Usa tagId1 o tagId2 (dovrebbero essere uguali)
var tagId = tagId1 > 0 ? tagId1 : tagId2;
if (tagId > 0 && !string.IsNullOrWhiteSpace(name))
{
categories.Add(new BidooCategoryInfo
{
TabId = 4,
TagId = tagId,
Slug = slug,
DisplayName = name,
IsSpecialCategory = false
});
}
}
}
// Se non abbiamo trovato categorie dal parsing, usa lista predefinita
if (categories.Count <= 3)
{
categories.AddRange(GetDefaultCategories());
}
// Aggiorna cache
_cachedCategories.Clear();
_cachedCategories.AddRange(categories);
_categoriesCachedAt = DateTime.UtcNow;
Console.WriteLine($"[BidooBrowser] Caricate {categories.Count} categorie");
}
catch (Exception ex)
{
Console.WriteLine($"[BidooBrowser] Errore caricamento categorie: {ex.Message}");
// Fallback a categorie predefinite
if (_cachedCategories.Count == 0)
{
categories.AddRange(GetDefaultCategories());
_cachedCategories.AddRange(categories);
}
else
{
return _cachedCategories.ToList();
}
}
return categories;
}
/// <summary>
/// Categorie predefinite come fallback
/// </summary>
private static List<BidooCategoryInfo> GetDefaultCategories()
{
return new List<BidooCategoryInfo>
{
new() { TabId = 4, TagId = 6, DisplayName = "Buoni", Slug = "buoni" },
new() { TabId = 4, TagId = 5, DisplayName = "Smartphone", Slug = "smartphone" },
new() { TabId = 4, TagId = 7, DisplayName = "Apple", Slug = "apple" },
new() { TabId = 4, TagId = 13, DisplayName = "Bellezza", Slug = "bellezza" },
new() { TabId = 4, TagId = 8, DisplayName = "Cucina", Slug = "cucina" },
new() { TabId = 4, TagId = 18, DisplayName = "Casa & Giardino", Slug = "casa_e_giardino" },
new() { TabId = 4, TagId = 11, DisplayName = "Elettrodomestici", Slug = "elettrodomestici" },
new() { TabId = 4, TagId = 9, DisplayName = "Videogame", Slug = "videogame" },
new() { TabId = 4, TagId = 41, DisplayName = "Giocattoli", Slug = "giocattoli" },
new() { TabId = 4, TagId = 14, DisplayName = "Tablet e PC", Slug = "tablet-e-pc" },
new() { TabId = 4, TagId = 20, DisplayName = "Hobby", Slug = "hobby" },
new() { TabId = 4, TagId = 22, DisplayName = "Smartwatch", Slug = "smartwatch" },
new() { TabId = 4, TagId = 37, DisplayName = "Animali Domestici", Slug = "animali_domestici" },
new() { TabId = 4, TagId = 12, DisplayName = "Moda", Slug = "moda" },
new() { TabId = 4, TagId = 10, DisplayName = "Smart TV", Slug = "smart-tv" },
new() { TabId = 4, TagId = 21, DisplayName = "Fai da Te", Slug = "fai_da_te" },
new() { TabId = 4, TagId = 26, DisplayName = "Luxury", Slug = "luxury" },
new() { TabId = 4, TagId = 19, DisplayName = "Cuffie e Audio", Slug = "cuffie-e-audio" },
new() { TabId = 4, TagId = 23, DisplayName = "Back to school", Slug = "back-to-school" },
new() { TabId = 4, TagId = 38, DisplayName = "Prima Infanzia", Slug = "prima-infanzia" }
};
}
/// <summary>
/// Ottiene le aste di una categoria specifica
/// Bidoo usa un sistema AJAX per caricare le aste dinamicamente
/// </summary>
public async Task<List<BidooBrowserAuction>> GetAuctionsAsync(
BidooCategoryInfo category,
int page = 0,
CancellationToken cancellationToken = default)
{
var auctions = new List<BidooBrowserAuction>();
try
{
// Bidoo carica le aste tramite chiamata AJAX a index.php con parametri POST-like in query string
// Il pattern è: index.php?selectBids=1&tab=X&tag=Y&offset=Z
string url;
if (category.IsSpecialCategory)
{
// Categorie speciali: BIDS (1), ALL (3), MANUAL (5)
var tabValue = category.TabId;
url = $"https://it.bidoo.com/index.php?selectBids=1&tab={tabValue}&tag=0&offset={page * 20}";
}
else
{
// Categorie normali: tab=4 + tag specifico
url = $"https://it.bidoo.com/index.php?selectBids=1&tab=4&tag={category.TagId}&offset={page * 20}";
}
Console.WriteLine($"[BidooBrowser] Fetching category '{category.DisplayName}': {url}");
var request = new HttpRequestMessage(HttpMethod.Get, url);
AddBrowserHeaders(request, "https://it.bidoo.com/");
request.Headers.Add("X-Requested-With", "XMLHttpRequest");
var response = await _httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
var html = await response.Content.ReadAsStringAsync(cancellationToken);
// Parse aste dall'HTML (fragment AJAX)
auctions = ParseAuctionsFromHtml(html);
Console.WriteLine($"[BidooBrowser] Trovate {auctions.Count} aste nella categoria {category.DisplayName}");
// ?? DEBUG: Verifica quante aste hanno IsCreditAuction = true
if (category.IsSpecialCategory && category.TabId == 1)
{
var creditCount = auctions.Count(a => a.IsCreditAuction);
Console.WriteLine($"[BidooBrowser] DEBUG Aste di Puntate: {creditCount}/{auctions.Count} hanno IsCreditAuction=true");
// Log primi 3 nomi per debug
foreach (var a in auctions.Take(3))
{
Console.WriteLine($"[BidooBrowser] - {a.Name} (ID: {a.AuctionId}, IsCreditAuction: {a.IsCreditAuction})");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[BidooBrowser] Errore caricamento aste: {ex.Message}");
}
return auctions;
}
private static string GetTabName(int tabId)
{
return tabId switch
{
1 => "BIDS",
2 => "FAV",
3 => "ALL",
5 => "MANUAL",
_ => "ALL"
};
}
/// <summary>
/// Parsa le aste dall'HTML della pagina
/// </summary>
private List<BidooBrowserAuction> ParseAuctionsFromHtml(string html)
{
var auctions = new List<BidooBrowserAuction>();
try
{
// Pattern per estrarre i div delle aste
// <div id="divAsta85584421" class="..." data-id="85584421" data-url="27_Puntate_85584421" data-freq="8" ...>
var auctionDivPattern = new Regex(
@"<div\s+id=""divAsta(\d+)""[^>]*" +
@"data-id=""(\d+)""[^>]*" +
@"data-url=""([^""]+)""[^>]*" +
@"data-freq=""(\d+)""[^>]*" +
@"(?:data-credit=""(\d+)"")?[^>]*" +
@"(?:data-credit-value=""(\d+)"")?[^>]*" +
@"(?:data-id-product=""(\d+)"")?",
RegexOptions.IgnoreCase | RegexOptions.Singleline);
// Pattern alternativo più semplice per catturare attributi
var simplePattern = new Regex(
@"<div[^>]+id=""divAsta(\d+)""[^>]*>",
RegexOptions.IgnoreCase);
var divMatches = simplePattern.Matches(html);
foreach (Match divMatch in divMatches)
{
if (!divMatch.Success) continue;
var auctionId = divMatch.Groups[1].Value;
// Trova il blocco completo dell'asta
var startIndex = divMatch.Index;
var endPattern = @"<!--/ \.bid -->";
var endIndex = html.IndexOf(endPattern, startIndex);
if (endIndex < 0) endIndex = html.IndexOf("</div><!--", startIndex + 1000);
if (endIndex < 0) continue;
var auctionHtml = html.Substring(startIndex, Math.Min(endIndex - startIndex + 100, html.Length - startIndex));
var auction = ParseSingleAuction(auctionId, auctionHtml);
if (auction != null)
{
auctions.Add(auction);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[BidooBrowser] Errore parsing HTML: {ex.Message}");
}
return auctions;
}
/// <summary>
/// Parsa una singola asta dal suo blocco HTML
/// </summary>
private BidooBrowserAuction? ParseSingleAuction(string auctionId, string html)
{
try
{
var auction = new BidooBrowserAuction { AuctionId = auctionId };
// Estrai data-url
var urlMatch = Regex.Match(html, @"data-url=""([^""]+)""");
if (urlMatch.Success)
{
auction.Url = $"https://it.bidoo.com/auction.php?a={urlMatch.Groups[1].Value}";
}
// Estrai data-freq
var freqMatch = Regex.Match(html, @"data-freq=""(\d+)""");
if (freqMatch.Success && int.TryParse(freqMatch.Groups[1].Value, out int freq))
{
auction.TimerFrequency = freq;
}
// Estrai data-credit e data-credit-value
var creditMatch = Regex.Match(html, @"data-credit=""(\d+)""");
if (creditMatch.Success && creditMatch.Groups[1].Value == "1")
{
auction.IsCreditAuction = true;
}
var creditValueMatch = Regex.Match(html, @"data-credit-value=""(\d+)""");
if (creditValueMatch.Success && int.TryParse(creditValueMatch.Groups[1].Value, out int creditVal))
{
auction.CreditValue = creditVal;
}
// Estrai data-id-product
var productMatch = Regex.Match(html, @"data-id-product=""(\d+)""");
if (productMatch.Success && int.TryParse(productMatch.Groups[1].Value, out int productId))
{
auction.ProductId = productId;
}
// Estrai immagine
var imgMatch = Regex.Match(html, @"<img[^>]+class=""img_small[^""]*""[^>]+src=""([^""]+)""");
if (imgMatch.Success)
{
auction.ImageUrl = imgMatch.Groups[1].Value;
}
else
{
// Pattern alternativo
imgMatch = Regex.Match(html, @"src=""(https://[^""]+/products/[^""]+)""");
if (imgMatch.Success)
{
auction.ImageUrl = imgMatch.Groups[1].Value;
}
}
// Estrai nome prodotto
var nameMatch = Regex.Match(html, @"<a[^>]+class=""name[^""]*""[^>]*>([^<]+)</a>", RegexOptions.IgnoreCase);
if (nameMatch.Success)
{
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
var buyNowMatch = Regex.Match(html, @"buy-rapid-now[^>]*>[^<]*<i[^>]*></i>\s*([0-9,\.]+)\s*€", RegexOptions.IgnoreCase);
if (buyNowMatch.Success)
{
var priceStr = buyNowMatch.Groups[1].Value.Replace(",", ".").Trim();
if (decimal.TryParse(priceStr, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out decimal buyNow))
{
auction.BuyNowPrice = buyNow;
}
}
// Controlla se è manuale (bi-noauto)
auction.IsManualOnly = html.Contains("bi-noauto", StringComparison.OrdinalIgnoreCase);
// Prezzo e bidder verranno aggiornati dalla chiamata a data.php
auction.CurrentPrice = 0.01m;
auction.LastBidder = "";
auction.RemainingSeconds = auction.TimerFrequency;
return auction;
}
catch (Exception ex)
{
Console.WriteLine($"[BidooBrowser] Errore parsing asta {auctionId}: {ex.Message}");
return null;
}
}
/// <summary>
/// Aggiorna lo stato delle aste usando data.php con LISTID (polling multiplo)
/// Formato chiamata: data.php?LISTID=id1,id2,id3&chk=timestamp
/// Formato risposta: timestamp*(id;status;expiry;price;bidder;timer;countdown#id2;...)
/// </summary>
public async Task UpdateAuctionStatesAsync(List<BidooBrowserAuction> auctions, CancellationToken cancellationToken = default)
{
if (auctions.Count == 0) return;
try
{
// Costruisci la lista di ID per il polling (formato LISTID)
var auctionIds = string.Join(",", auctions.Select(a => a.AuctionId));
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var url = $"https://it.bidoo.com/data.php?LISTID={auctionIds}&chk={timestamp}";
Console.WriteLine($"[BidooBrowser] Polling {auctions.Count} aste...");
var request = new HttpRequestMessage(HttpMethod.Get, url);
AddBrowserHeaders(request, "https://it.bidoo.com/");
request.Headers.Add("X-Requested-With", "XMLHttpRequest");
var response = await _httpClient.SendAsync(request, cancellationToken);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"[BidooBrowser] Polling fallito: {response.StatusCode}");
return;
}
var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
// Parse risposta formato LISTID
ParseListIdResponse(responseText, auctions);
foreach (var auction in auctions)
{
auction.LastUpdated = DateTime.UtcNow;
}
}
catch (Exception ex)
{
Console.WriteLine($"[BidooBrowser] Errore aggiornamento stati: {ex.Message}");
}
}
/// <summary>
/// Parsa la risposta di data.php formato LISTID
/// Formato: serverTimestamp*(id;status;expiry;price;;#id2;status2;...)
/// Esempio: 1769073106*(85559629;ON;1769082240;1;;#85559630;ON;1769082240;1;;)
/// Il timestamp del server viene usato come riferimento per calcolare il tempo rimanente
/// </summary>
private void ParseListIdResponse(string response, List<BidooBrowserAuction> auctions)
{
try
{
// Trova inizio dati dopo timestamp*
var starIndex = response.IndexOf('*');
if (starIndex == -1)
{
Console.WriteLine("[BidooBrowser] Risposta non valida: manca '*'");
return;
}
// Estrai il timestamp del server (prima di *)
var serverTimestampStr = response.Substring(0, starIndex);
long serverTimestamp = 0;
long.TryParse(serverTimestampStr, out serverTimestamp);
var mainData = response.Substring(starIndex + 1);
// Rimuovi parentesi se presenti
if (mainData.StartsWith("(") && mainData.EndsWith(")"))
{
mainData = mainData.Substring(1, mainData.Length - 2);
}
// Split per ogni asta (separatore #)
var auctionEntries = mainData.Split('#', StringSplitOptions.RemoveEmptyEntries);
int updatedCount = 0;
foreach (var entry in auctionEntries)
{
// Formato: id;status;expiry;price;; (bidder e timer possono essere vuoti)
var fields = entry.Split(';');
if (fields.Length < 4) continue;
var id = fields[0].Trim();
var status = fields[1].Trim(); // ON/OFF
var expiryStr = fields[2].Trim(); // timestamp scadenza (stesso formato del server)
var priceStr = fields[3].Trim(); // prezzo (centesimi)
var bidder = fields.Length > 4 ? fields[4].Trim() : ""; // ultimo bidder (può essere vuoto)
var auction = auctions.FirstOrDefault(a => a.AuctionId == id);
if (auction == null) continue;
// Aggiorna prezzo (è in centesimi, convertire in euro)
if (int.TryParse(priceStr, out int priceCents))
{
auction.CurrentPrice = priceCents / 100m;
}
// Aggiorna bidder solo se non vuoto
if (!string.IsNullOrEmpty(bidder))
{
auction.LastBidder = bidder;
}
// Calcola tempo rimanente usando il timestamp del server come riferimento
if (long.TryParse(expiryStr, out long expiryTimestamp) && serverTimestamp > 0)
{
// Il tempo rimanente è: expiry - serverTime (entrambi nello stesso formato)
var remainingSeconds = expiryTimestamp - serverTimestamp;
auction.RemainingSeconds = remainingSeconds > 0 ? (int)remainingSeconds : 0;
}
else if (status == "ON")
{
// Se non riusciamo a calcolare, usa il timer frequency come fallback
if (auction.RemainingSeconds <= 0)
{
auction.RemainingSeconds = auction.TimerFrequency;
}
}
// Status: ON = attiva in countdown, OFF = terminata/in pausa
auction.IsActive = status == "ON";
auction.IsSold = status != "ON" && auction.RemainingSeconds <= 0;
updatedCount++;
}
Console.WriteLine($"[BidooBrowser] Aggiornate {updatedCount} aste su {auctionEntries.Length}");
}
catch (Exception ex)
{
Console.WriteLine($"[BidooBrowser] Errore parsing LISTID response: {ex.Message}");
}
}
/// <summary>
/// Converte countdown string in secondi
/// Formati: "7m", "1h 16m", "00:08", vuoto (usa timer frequency)
/// </summary>
private int ParseCountdown(string countdown, int defaultSeconds)
{
if (string.IsNullOrWhiteSpace(countdown))
{
return defaultSeconds;
}
try
{
// Formato ore e minuti: "1h 16m"
var hourMatch = Regex.Match(countdown, @"(\d+)h");
var minMatch = Regex.Match(countdown, @"(\d+)m");
int totalSeconds = 0;
if (hourMatch.Success && int.TryParse(hourMatch.Groups[1].Value, out int hours))
{
totalSeconds += hours * 3600;
}
if (minMatch.Success && int.TryParse(minMatch.Groups[1].Value, out int mins))
{
totalSeconds += mins * 60;
}
if (totalSeconds > 0)
{
return totalSeconds;
}
// Formato "00:08" (mm:ss o ss)
if (countdown.Contains(":"))
{
var parts = countdown.Split(':');
if (parts.Length == 2 &&
int.TryParse(parts[0], out int p1) &&
int.TryParse(parts[1], out int p2))
{
return p1 * 60 + p2;
}
}
// Solo numero = secondi
if (int.TryParse(countdown, out int secs))
{
return secs;
}
}
catch { }
return defaultSeconds;
}
/// <summary>
/// Carica nuove aste usando get_auction_updates.php (simula scrolling infinito)
/// Questa API restituisce aste che non sono ancora state caricate
/// </summary>
public async Task<List<BidooBrowserAuction>> GetMoreAuctionsAsync(
BidooCategoryInfo category,
List<string> existingAuctionIds,
CancellationToken cancellationToken = default)
{
var newAuctions = new List<BidooBrowserAuction>();
try
{
var existingIdsSet = existingAuctionIds.ToHashSet();
// Prepara la chiamata POST a get_auction_updates.php
var url = "https://it.bidoo.com/get_auction_updates.php";
// Costruisci il body della richiesta
var viewIds = string.Join(",", existingAuctionIds);
var tabValue = category.IsSpecialCategory ? category.TabId : 4;
var tagValue = category.IsSpecialCategory ? 0 : category.TagId;
var formContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("prefetch", "true"),
new KeyValuePair<string, string>("view", viewIds),
new KeyValuePair<string, string>("tab", tabValue.ToString()),
new KeyValuePair<string, string>("tag", tagValue.ToString())
});
var request = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = formContent
};
AddBrowserHeaders(request, "https://it.bidoo.com/");
request.Headers.Add("X-Requested-With", "XMLHttpRequest");
Console.WriteLine($"[BidooBrowser] Fetching more auctions with {existingAuctionIds.Count} existing IDs...");
var response = await _httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
// Parse la risposta JSON
// Formato: {"gc":[],"int":[],"list":[id1,id2,...],"items":["<html>","<html>",...]}
newAuctions = ParseGetAuctionUpdatesResponse(responseText, existingIdsSet);
Console.WriteLine($"[BidooBrowser] Trovate {newAuctions.Count} nuove aste");
}
catch (Exception ex)
{
Console.WriteLine($"[BidooBrowser] Errore caricamento nuove aste: {ex.Message}");
}
return newAuctions;
}
/// <summary>
/// Parsa la risposta di get_auction_updates.php
/// </summary>
private List<BidooBrowserAuction> ParseGetAuctionUpdatesResponse(string json, HashSet<string> existingIds)
{
var auctions = new List<BidooBrowserAuction>();
try
{
// Parse JSON manuale per estrarre items[]
// Cerchiamo "items":["...","..."]
var itemsMatch = Regex.Match(json, @"""items"":\s*\[(.*?)\](?=,""|\})", RegexOptions.Singleline);
if (!itemsMatch.Success)
{
Console.WriteLine("[BidooBrowser] Nessun items trovato nella risposta");
return auctions;
}
var itemsContent = itemsMatch.Groups[1].Value;
// Gli items sono stringhe HTML escaped, dobbiamo parsarle
// Ogni item è una stringa JSON che contiene HTML
var htmlPattern = new Regex(@"""((?:[^""\\]|\\.)*?)""", RegexOptions.Singleline);
var htmlMatches = htmlPattern.Matches(itemsContent);
foreach (Match htmlMatch in htmlMatches)
{
if (!htmlMatch.Success) continue;
// Unescape la stringa JSON
var escapedHtml = htmlMatch.Groups[1].Value;
var html = UnescapeJsonString(escapedHtml);
// Estrai l'ID dell'asta
var idMatch = Regex.Match(html, @"id=""divAsta(\d+)""");
if (!idMatch.Success) continue;
var auctionId = idMatch.Groups[1].Value;
// Salta se già esiste
if (existingIds.Contains(auctionId)) continue;
// Parsa l'asta dall'HTML
var auction = ParseSingleAuction(auctionId, html);
if (auction != null)
{
auctions.Add(auction);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[BidooBrowser] Errore parsing get_auction_updates response: {ex.Message}");
}
return auctions;
}
/// <summary>
/// Unescape di una stringa JSON
/// </summary>
private static string UnescapeJsonString(string escaped)
{
return escaped
.Replace("\\/", "/")
.Replace("\\n", "\n")
.Replace("\\r", "\r")
.Replace("\\t", "\t")
.Replace("\\\"", "\"")
.Replace("\\\\", "\\");
}
}
}
File diff suppressed because it is too large Load Diff
+22 -3
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,
@@ -0,0 +1,352 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AutoBidder.Models;
namespace AutoBidder.Services
{
/// <summary>
/// Servizio per calcolare statistiche aggregate per prodotto e generare limiti consigliati.
/// L'algoritmo analizza le aste storiche per determinare i parametri ottimali.
/// </summary>
public class ProductStatisticsService
{
private readonly DatabaseService _db;
public ProductStatisticsService(DatabaseService db)
{
_db = db;
}
/// <summary>
/// Genera una chiave univoca normalizzata per raggruppare prodotti simili.
/// Rimuove varianti, numeri di serie, colori ecc.
/// </summary>
public static string GenerateProductKey(string productName)
{
if (string.IsNullOrWhiteSpace(productName))
return "unknown";
var normalized = productName.ToLowerInvariant().Trim();
// Rimuovi contenuto tra parentesi (varianti, colori, capacità)
normalized = Regex.Replace(normalized, @"\([^)]*\)", "");
normalized = Regex.Replace(normalized, @"\[[^\]]*\]", "");
// Rimuovi colori comuni
var colors = new[] { "nero", "bianco", "grigio", "rosso", "blu", "verde", "oro", "argento",
"black", "white", "gray", "red", "blue", "green", "gold", "silver",
"space gray", "midnight", "starlight" };
foreach (var color in colors)
{
normalized = Regex.Replace(normalized, $@"\b{color}\b", "", RegexOptions.IgnoreCase);
}
// Rimuovi capacità storage (64gb, 128gb, 256gb, ecc.)
normalized = Regex.Replace(normalized, @"\b\d+\s*(gb|tb|mb)\b", "", RegexOptions.IgnoreCase);
// Rimuovi numeri di serie e codici prodotto
normalized = Regex.Replace(normalized, @"\b[A-Z]{2,}\d{3,}\b", "", RegexOptions.IgnoreCase);
// Normalizza spazi e caratteri speciali
normalized = Regex.Replace(normalized, @"[^a-z0-9\s]", " ");
normalized = Regex.Replace(normalized, @"\s+", "_");
normalized = normalized.Trim('_');
// Limita lunghezza
if (normalized.Length > 50)
normalized = normalized.Substring(0, 50);
return string.IsNullOrEmpty(normalized) ? "unknown" : normalized;
}
/// <summary>
/// Aggiorna le statistiche aggregate per un prodotto dopo una nuova asta completata
/// </summary>
public async Task UpdateProductStatisticsAsync(string productKey, string productName)
{
try
{
// Ottieni tutti i risultati per questo prodotto
var results = await _db.GetAuctionResultsByProductAsync(productKey, 500);
if (results.Count == 0)
{
Console.WriteLine($"[ProductStats] No results found for product: {productKey}");
return;
}
// Calcola statistiche aggregate
var wonResults = results.Where(r => r.Won).ToList();
var lostResults = results.Where(r => !r.Won).ToList();
var stats = new ProductStatisticsRecord
{
ProductKey = productKey,
ProductName = productName,
TotalAuctions = results.Count,
WonAuctions = wonResults.Count,
LostAuctions = lostResults.Count
};
// Statistiche prezzo (usa aste vinte per calcolare i target)
if (wonResults.Any())
{
stats.AvgFinalPrice = wonResults.Average(r => r.FinalPrice);
stats.MinFinalPrice = wonResults.Min(r => r.FinalPrice);
stats.MaxFinalPrice = wonResults.Max(r => r.FinalPrice);
stats.MedianFinalPrice = CalculateMedian(wonResults.Select(r => r.FinalPrice).ToList());
}
else if (results.Any())
{
stats.AvgFinalPrice = results.Average(r => r.FinalPrice);
stats.MinFinalPrice = results.Min(r => r.FinalPrice);
stats.MaxFinalPrice = results.Max(r => r.FinalPrice);
stats.MedianFinalPrice = CalculateMedian(results.Select(r => r.FinalPrice).ToList());
}
// Statistiche puntate (usa WinnerBidsUsed se disponibile, altrimenti BidsUsed)
var bidsData = wonResults
.Where(r => r.WinnerBidsUsed.HasValue || r.BidsUsed > 0)
.Select(r => r.WinnerBidsUsed ?? r.BidsUsed)
.ToList();
if (bidsData.Any())
{
stats.AvgBidsToWin = bidsData.Select(b => (double)b).Average();
stats.MinBidsToWin = bidsData.Min();
stats.MaxBidsToWin = bidsData.Max();
}
// Statistiche reset
var resetData = wonResults.Where(r => r.TotalResets.HasValue).Select(r => r.TotalResets!.Value).ToList();
if (resetData.Any())
{
stats.AvgResets = resetData.Select(r => (double)r).Average();
stats.MinResets = resetData.Min();
stats.MaxResets = resetData.Max();
}
// Calcola limiti consigliati
var limits = CalculateRecommendedLimits(results);
stats.RecommendedMinPrice = limits.MinPrice;
stats.RecommendedMaxPrice = limits.MaxPrice;
stats.RecommendedMinResets = limits.MinResets;
stats.RecommendedMaxResets = limits.MaxResets;
stats.RecommendedMaxBids = limits.MaxBids;
// Calcola statistiche per fascia oraria
var hourlyStats = CalculateHourlyStats(results);
stats.HourlyStatsJson = JsonSerializer.Serialize(hourlyStats);
// Salva nel database
await _db.UpsertProductStatisticsAsync(stats);
Console.WriteLine($"[ProductStats] Updated stats for {productKey}: {stats.TotalAuctions} auctions, WinRate={stats.WinRate:F1}%");
}
catch (Exception ex)
{
Console.WriteLine($"[ProductStats ERROR] Failed to update stats for {productKey}: {ex.Message}");
}
}
/// <summary>
/// Calcola i limiti consigliati basandosi sui dati storici
/// </summary>
public RecommendedLimits CalculateRecommendedLimits(List<AutoBidder.Models.AuctionResultExtended> results)
{
var limits = new RecommendedLimits
{
SampleSize = results.Count
};
if (results.Count < 3)
{
limits.ConfidenceScore = 0;
return limits;
}
var wonResults = results.Where(r => r.Won).ToList();
if (wonResults.Count == 0)
{
// Nessuna vittoria: usa tutti i risultati con margine conservativo
limits.ConfidenceScore = 10;
limits.MinPrice = results.Min(r => r.FinalPrice) * 0.8;
limits.MaxPrice = results.Max(r => r.FinalPrice) * 1.2;
return limits;
}
// Calcola percentili sui prezzi delle aste vinte
var prices = wonResults.Select(r => r.FinalPrice).OrderBy(p => p).ToList();
limits.MinPrice = CalculatePercentile(prices, 10); // 10° percentile - entrare presto
limits.MaxPrice = CalculatePercentile(prices, 90); // 90° percentile - limite sicuro
// Calcola limiti reset
var resets = wonResults.Where(r => r.TotalResets.HasValue).Select(r => r.TotalResets!.Value).ToList();
if (resets.Any())
{
var avgResets = resets.Average();
var stdDev = CalculateStandardDeviation(resets.Select(r => (double)r).ToList());
limits.MinResets = Math.Max(0, (int)(avgResets - stdDev)); // Media - 1 stddev
limits.MaxResets = (int)(avgResets + stdDev); // Media + 1 stddev
}
// Calcola limiti puntate
var bids = wonResults
.Where(r => r.WinnerBidsUsed.HasValue || r.BidsUsed > 0)
.Select(r => r.WinnerBidsUsed ?? r.BidsUsed)
.OrderBy(b => b)
.ToList();
if (bids.Any())
{
// 90° percentile + 10% buffer
limits.MaxBids = (int)(CalculatePercentile(bids.Select(b => (double)b).ToList(), 90) * 1.1);
}
// Trova la fascia oraria migliore
var hourlyWins = wonResults
.Where(r => r.ClosedAtHour.HasValue)
.GroupBy(r => r.ClosedAtHour!.Value)
.Select(g => new { Hour = g.Key, Wins = g.Count() })
.OrderByDescending(x => x.Wins)
.FirstOrDefault();
if (hourlyWins != null)
{
limits.BestHourToPlay = hourlyWins.Hour;
}
// Win rate
limits.AverageWinRate = results.Count > 0 ? (double)wonResults.Count / results.Count * 100 : 0;
// Confidence score basato sul sample size
limits.ConfidenceScore = results.Count switch
{
>= 50 => 95,
>= 30 => 85,
>= 20 => 70,
>= 10 => 50,
>= 5 => 30,
_ => 15
};
return limits;
}
/// <summary>
/// Calcola statistiche aggregate per ogni fascia oraria
/// </summary>
private List<HourlyStats> CalculateHourlyStats(List<AutoBidder.Models.AuctionResultExtended> results)
{
var stats = new List<HourlyStats>();
var grouped = results
.Where(r => r.ClosedAtHour.HasValue)
.GroupBy(r => r.ClosedAtHour!.Value);
foreach (var group in grouped)
{
var hourResults = group.ToList();
var wonInHour = hourResults.Where(r => r.Won).ToList();
stats.Add(new HourlyStats
{
Hour = group.Key,
TotalAuctions = hourResults.Count,
WonAuctions = wonInHour.Count,
AvgFinalPrice = hourResults.Any() ? hourResults.Average(r => r.FinalPrice) : 0,
AvgBidsUsed = hourResults.Any() ? hourResults.Average(r => r.BidsUsed) : 0
});
}
return stats.OrderBy(s => s.Hour).ToList();
}
/// <summary>
/// Ottiene le statistiche per un prodotto
/// </summary>
public async Task<ProductStatisticsRecord?> GetProductStatisticsAsync(string productKey)
{
return await _db.GetProductStatisticsAsync(productKey);
}
/// <summary>
/// Ottiene tutti i prodotti con statistiche
/// </summary>
public async Task<List<ProductStatisticsRecord>> GetAllProductStatisticsAsync()
{
return await _db.GetAllProductStatisticsAsync();
}
/// <summary>
/// Ottiene i limiti consigliati per un prodotto
/// </summary>
public async Task<RecommendedLimits?> GetRecommendedLimitsAsync(string productKey)
{
var stats = await _db.GetProductStatisticsAsync(productKey);
if (stats == null)
return null;
return new RecommendedLimits
{
MinPrice = stats.RecommendedMinPrice ?? 0,
MaxPrice = stats.RecommendedMaxPrice ?? 0,
MinResets = stats.RecommendedMinResets ?? 0,
MaxResets = stats.RecommendedMaxResets ?? 0,
MaxBids = stats.RecommendedMaxBids ?? 0,
ConfidenceScore = stats.TotalAuctions switch
{
>= 50 => 95,
>= 30 => 85,
>= 20 => 70,
>= 10 => 50,
>= 5 => 30,
_ => 15
},
SampleSize = stats.TotalAuctions,
AverageWinRate = stats.WinRate
};
}
// Helpers per calcoli statistici
private static double CalculatePercentile(List<double> sortedData, int percentile)
{
if (sortedData.Count == 0) return 0;
if (sortedData.Count == 1) return sortedData[0];
double index = (percentile / 100.0) * (sortedData.Count - 1);
int lower = (int)Math.Floor(index);
int upper = (int)Math.Ceiling(index);
if (lower == upper) return sortedData[lower];
return sortedData[lower] + (sortedData[upper] - sortedData[lower]) * (index - lower);
}
private static double CalculateStandardDeviation(List<double> data)
{
if (data.Count < 2) return 0;
double avg = data.Average();
double sumSquares = data.Sum(d => Math.Pow(d - avg, 2));
return Math.Sqrt(sumSquares / (data.Count - 1));
}
private static double CalculateMedian(List<double> data)
{
if (data.Count == 0) return 0;
var sorted = data.OrderBy(x => x).ToList();
int mid = sorted.Count / 2;
return sorted.Count % 2 == 0
? (sorted[mid - 1] + sorted[mid]) / 2.0
: sorted[mid];
}
}
}
+283 -220
View File
@@ -2,64 +2,145 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using AutoBidder.Models;
using AutoBidder.Data;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
namespace AutoBidder.Services
{
/// <summary>
/// Servizio per calcolo e gestione statistiche avanzate
/// Usa PostgreSQL per statistiche persistenti e SQLite locale come fallback
/// Servizio per calcolo e gestione statistiche.
/// Usa esclusivamente il database SQLite interno gestito da DatabaseService.
/// Le statistiche sono disabilitate se il database non è disponibile.
/// </summary>
public class StatsService
{
private readonly DatabaseService _db;
private readonly PostgresStatsContext? _postgresDb;
private readonly bool _postgresAvailable;
public StatsService(DatabaseService db, PostgresStatsContext? postgresDb = null)
/// <summary>
/// Indica se le statistiche sono disponibili (database SQLite funzionante)
/// </summary>
public bool IsAvailable => _db.IsAvailable && _db.IsInitialized;
/// <summary>
/// Messaggio di errore se le statistiche non sono disponibili
/// </summary>
public string? ErrorMessage => !IsAvailable ? _db.InitializationError ?? "Database non disponibile" : null;
/// <summary>
/// Path del database SQLite
/// </summary>
public string DatabasePath => _db.DatabasePath;
private ProductStatisticsService? _productStatsService;
public StatsService(DatabaseService db)
{
_db = db;
_postgresDb = postgresDb;
_postgresAvailable = false;
_productStatsService = new ProductStatisticsService(db);
// Verifica disponibilità PostgreSQL
if (_postgresDb != null)
// Log stato database SQLite
Console.WriteLine($"[StatsService] Database available: {_db.IsAvailable}");
Console.WriteLine($"[StatsService] Database initialized: {_db.IsInitialized}");
if (!_db.IsAvailable)
{
try
{
_postgresAvailable = _postgresDb.Database.CanConnect();
var status = _postgresAvailable ? "AVAILABLE" : "UNAVAILABLE";
Console.WriteLine($"[StatsService] PostgreSQL status: {status}");
}
catch (Exception ex)
{
Console.WriteLine($"[StatsService] PostgreSQL connection failed: {ex.Message}");
}
}
else
{
Console.WriteLine("[StatsService] PostgreSQL not configured - using SQLite only");
Console.WriteLine($"[StatsService] Database error: {_db.InitializationError}");
}
}
/// <summary>
/// Registra il completamento di un'asta (sia su PostgreSQL che SQLite)
/// Registra il completamento di un'asta con tutti i dati per analytics
/// Include scraping HTML per ottenere le puntate del vincitore
/// </summary>
public async Task RecordAuctionCompletedAsync(AuctionInfo auction, bool won)
public async Task RecordAuctionCompletedAsync(AuctionInfo auction, AuctionState state, bool won)
{
// Skip se database non disponibile
if (!IsAvailable)
{
Console.WriteLine("[StatsService] Skipping record - database not available");
return;
}
try
{
Console.WriteLine($"[StatsService] ====== INIZIO SALVATAGGIO ASTA TERMINATA ======");
Console.WriteLine($"[StatsService] Asta: {auction.Name} (ID: {auction.AuctionId})");
Console.WriteLine($"[StatsService] Stato: {(won ? "VINTA" : "PERSA")}");
var today = DateTime.UtcNow.ToString("yyyy-MM-dd");
var bidsUsed = auction.BidsUsedOnThisAuction ?? 0;
var bidCost = auction.BidCost;
var moneySpent = bidsUsed * bidCost;
var finalPrice = auction.LastState?.Price ?? 0;
var finalPrice = state.Price;
var buyNowPrice = auction.BuyNowPrice;
var shippingCost = auction.ShippingCost ?? 0;
// Dati aggiuntivi per analytics
var winnerUsername = state.LastBidder;
var totalResets = auction.ResetCount;
var productKey = ProductStatisticsService.GenerateProductKey(auction.Name);
Console.WriteLine($"[StatsService] Prezzo finale: €{finalPrice:F2}");
Console.WriteLine($"[StatsService] Puntate usate (utente): {bidsUsed}");
Console.WriteLine($"[StatsService] Vincitore: {winnerUsername ?? "N/A"}");
Console.WriteLine($"[StatsService] Reset totali: {totalResets}");
// ?? SCRAPING HTML: Ottieni puntate del vincitore dalla pagina dell'asta
int? winnerBidsUsed = null;
if (!string.IsNullOrEmpty(winnerUsername))
{
Console.WriteLine($"[StatsService] Avvio scraping HTML per ottenere puntate del vincitore...");
winnerBidsUsed = await ScrapeWinnerBidsFromAuctionPageAsync(auction.OriginalUrl);
// ? VALIDAZIONE: Verifica che i dati estratti siano ragionevoli
if (winnerBidsUsed.HasValue)
{
if (winnerBidsUsed.Value < 0)
{
Console.WriteLine($"[StatsService] ? VALIDAZIONE FALLITA: Puntate negative ({winnerBidsUsed.Value}) - Dato scartato");
winnerBidsUsed = null;
}
else if (winnerBidsUsed.Value > 50000)
{
Console.WriteLine($"[StatsService] ? VALIDAZIONE FALLITA: Puntate sospette ({winnerBidsUsed.Value} > 50000) - Dato scartato");
winnerBidsUsed = null;
}
else if (winnerBidsUsed.Value == 0 && totalResets > 10)
{
Console.WriteLine($"[StatsService] ? VALIDAZIONE FALLITA: 0 puntate con {totalResets} reset - Dato sospetto");
winnerBidsUsed = null;
}
else
{
Console.WriteLine($"[StatsService] ? Puntate vincitore estratte da HTML: {winnerBidsUsed.Value} (validazione OK)");
}
}
// Fallback se validazione fallita o scraping non riuscito
if (!winnerBidsUsed.HasValue)
{
Console.WriteLine($"[StatsService] ? Impossibile estrarre puntate valide del vincitore da HTML");
// Fallback: conta da RecentBids (meno affidabile)
if (auction.RecentBids != null)
{
winnerBidsUsed = auction.RecentBids
.Count(b => b.Username?.Equals(winnerUsername, StringComparison.OrdinalIgnoreCase) == true);
if (winnerBidsUsed.Value > 0)
{
Console.WriteLine($"[StatsService] Fallback: puntate vincitore da RecentBids: {winnerBidsUsed.Value}");
}
else
{
Console.WriteLine($"[StatsService] ? Fallback fallito: nessuna puntata trovata in RecentBids");
winnerBidsUsed = null;
}
}
}
}
double? totalCost = null;
double? savings = null;
@@ -67,9 +148,14 @@ namespace AutoBidder.Services
{
totalCost = finalPrice + moneySpent + shippingCost;
savings = (buyNowPrice.Value + shippingCost) - totalCost.Value;
Console.WriteLine($"[StatsService] Costo totale: €{totalCost:F2}");
Console.WriteLine($"[StatsService] Risparmio: €{savings:F2}");
}
// Salva su SQLite (sempre)
Console.WriteLine($"[StatsService] Salvataggio nel database...");
// Salva risultato asta con tutti i campi
await _db.SaveAuctionResultAsync(
auction.AuctionId,
auction.Name,
@@ -79,9 +165,16 @@ namespace AutoBidder.Services
buyNowPrice,
shippingCost,
totalCost,
savings
savings,
winnerUsername,
totalResets,
winnerBidsUsed,
productKey
);
Console.WriteLine($"[StatsService] ? Risultato asta salvato");
// Aggiorna statistiche giornaliere
await _db.SaveDailyStatAsync(
today,
bidsUsed,
@@ -89,229 +182,178 @@ namespace AutoBidder.Services
won ? 1 : 0,
won ? 0 : 1,
savings ?? 0,
auction.LastState?.PollingLatencyMs
state.PollingLatencyMs
);
// Salva su PostgreSQL se disponibile
if (_postgresAvailable && _postgresDb != null)
Console.WriteLine($"[StatsService] ? Statistiche giornaliere aggiornate");
// Aggiorna statistiche aggregate per prodotto
if (_productStatsService != null)
{
await SaveToPostgresAsync(auction, won, finalPrice, bidsUsed, totalCost, savings);
Console.WriteLine($"[StatsService] Aggiornamento statistiche prodotto (key: {productKey})...");
await _productStatsService.UpdateProductStatisticsAsync(productKey, auction.Name);
Console.WriteLine($"[StatsService] ? Statistiche prodotto aggiornate");
}
Console.WriteLine($"[StatsService] Recorded auction {auction.Name} - Won: {won}, Bids: {bidsUsed}, Savings: {savings:F2}€");
Console.WriteLine($"[StatsService] ====== SALVATAGGIO COMPLETATO ======");
}
catch (Exception ex)
{
Console.WriteLine($"[StatsService ERROR] Failed to record auction: {ex.Message}");
Console.WriteLine($"[StatsService ERROR] Stack: {ex.StackTrace}");
}
}
/// <summary>
/// Salva asta conclusa su PostgreSQL
/// Scarica l'HTML della pagina dell'asta e estrae le puntate del vincitore
/// </summary>
private async Task SaveToPostgresAsync(AuctionInfo auction, bool won, double finalPrice, int bidsUsed, double? totalCost, double? savings)
private async Task<int?> ScrapeWinnerBidsFromAuctionPageAsync(string auctionUrl)
{
if (_postgresDb == null) return;
try
{
var completedAuction = new CompletedAuction
{
AuctionId = auction.AuctionId,
ProductName = auction.Name,
FinalPrice = (decimal)finalPrice,
BuyNowPrice = auction.BuyNowPrice.HasValue ? (decimal)auction.BuyNowPrice.Value : null,
ShippingCost = auction.ShippingCost.HasValue ? (decimal)auction.ShippingCost.Value : null,
TotalBids = auction.LastState?.MyBidsCount ?? bidsUsed, // Usa MyBidsCount se disponibile
MyBidsCount = bidsUsed,
ResetCount = auction.ResetCount,
Won = won,
WinnerUsername = won ? "ME" : auction.LastState?.LastBidder,
CompletedAt = DateTime.UtcNow,
AverageLatency = auction.LastState != null ? (decimal)auction.LastState.PollingLatencyMs : null, // PollingLatencyMs è int, non nullable
Savings = savings.HasValue ? (decimal)savings.Value : null,
TotalCost = totalCost.HasValue ? (decimal)totalCost.Value : null,
CreatedAt = DateTime.UtcNow
};
_postgresDb.CompletedAuctions.Add(completedAuction);
await _postgresDb.SaveChangesAsync();
// Aggiorna statistiche prodotto
await UpdateProductStatisticsAsync(auction, won, bidsUsed, finalPrice);
using var httpClient = new HttpClient();
// ? RIDOTTO: Timeout da 10s ? 5s per evitare rallentamenti
httpClient.Timeout = TimeSpan.FromSeconds(5);
// Aggiorna metriche giornaliere
await UpdateDailyMetricsAsync(DateTime.UtcNow.Date, bidsUsed, auction.BidCost, won, savings ?? 0);
Console.WriteLine($"[PostgreSQL] Saved auction {auction.Name} to PostgreSQL");
}
catch (Exception ex)
{
Console.WriteLine($"[PostgreSQL ERROR] Failed to save auction: {ex.Message}");
}
}
/// <summary>
/// Aggiorna statistiche prodotto in PostgreSQL
/// </summary>
private async Task UpdateProductStatisticsAsync(AuctionInfo auction, bool won, int bidsUsed, double finalPrice)
{
if (_postgresDb == null) return;
try
{
var productKey = GenerateProductKey(auction.Name);
var stat = await _postgresDb.ProductStatistics.FirstOrDefaultAsync(p => p.ProductKey == productKey);
if (stat == null)
{
stat = new ProductStatistic
{
ProductKey = productKey,
ProductName = auction.Name,
TotalAuctions = 0,
MinBidsSeen = int.MaxValue,
MaxBidsSeen = 0,
CompetitionLevel = "Medium"
};
_postgresDb.ProductStatistics.Add(stat);
}
stat.TotalAuctions++;
stat.AverageFinalPrice = ((stat.AverageFinalPrice * (stat.TotalAuctions - 1)) + (decimal)finalPrice) / stat.TotalAuctions;
stat.AverageResets = ((stat.AverageResets * (stat.TotalAuctions - 1)) + auction.ResetCount) / stat.TotalAuctions;
// Headers browser-like per evitare rilevamento come bot
httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
httpClient.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
httpClient.DefaultRequestHeaders.Add("Accept-Language", "it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7");
if (won)
{
stat.AverageWinningBids = ((stat.AverageWinningBids * Math.Max(1, stat.TotalAuctions - 1)) + bidsUsed) / stat.TotalAuctions;
}
Console.WriteLine($"[StatsService] Downloading HTML from: {auctionUrl} (timeout: 5s)");
stat.MinBidsSeen = Math.Min(stat.MinBidsSeen, bidsUsed);
stat.MaxBidsSeen = Math.Max(stat.MaxBidsSeen, bidsUsed);
stat.RecommendedMaxBids = (int)(stat.AverageWinningBids * 1.5m); // 50% buffer
stat.RecommendedMaxPrice = stat.AverageFinalPrice * 1.2m; // 20% buffer
stat.LastUpdated = DateTime.UtcNow;
// Determina livello competizione
if (stat.AverageWinningBids > 50) stat.CompetitionLevel = "High";
else if (stat.AverageWinningBids < 20) stat.CompetitionLevel = "Low";
else stat.CompetitionLevel = "Medium";
await _postgresDb.SaveChangesAsync();
}
catch (Exception ex)
{
Console.WriteLine($"[PostgreSQL ERROR] Failed to update product statistics: {ex.Message}");
}
}
/// <summary>
/// Aggiorna metriche giornaliere in PostgreSQL
/// </summary>
private async Task UpdateDailyMetricsAsync(DateTime date, int bidsUsed, double bidCost, bool won, double savings)
{
if (_postgresDb == null) return;
try
{
var metric = await _postgresDb.DailyMetrics.FirstOrDefaultAsync(m => m.Date.Date == date.Date);
if (metric == null)
{
metric = new DailyMetric { Date = date.Date };
_postgresDb.DailyMetrics.Add(metric);
}
metric.TotalBidsUsed += bidsUsed;
metric.MoneySpent += (decimal)(bidsUsed * bidCost);
if (won) metric.AuctionsWon++; else metric.AuctionsLost++;
metric.TotalSavings += (decimal)savings;
var totalAuctions = metric.AuctionsWon + metric.AuctionsLost;
if (totalAuctions > 0)
{
metric.WinRate = ((decimal)metric.AuctionsWon / totalAuctions) * 100;
}
if (metric.MoneySpent > 0)
{
metric.ROI = (metric.TotalSavings / metric.MoneySpent) * 100;
}
await _postgresDb.SaveChangesAsync();
}
catch (Exception ex)
{
Console.WriteLine($"[PostgreSQL ERROR] Failed to update daily metrics: {ex.Message}");
}
}
/// <summary>
/// Genera chiave univoca per prodotto
/// </summary>
private string GenerateProductKey(string productName)
{
var normalized = productName.ToLowerInvariant()
.Replace(" ", "_")
.Replace("-", "_");
return System.Text.RegularExpressions.Regex.Replace(normalized, "[^a-z0-9_]", "");
}
/// <summary>
/// Ottiene raccomandazioni strategiche da PostgreSQL
/// </summary>
public async Task<List<StrategicInsight>> GetStrategicInsightsAsync(string? productKey = null)
{
if (!_postgresAvailable || _postgresDb == null)
{
return new List<StrategicInsight>();
}
try
{
var query = _postgresDb.StrategicInsights.Where(i => i.IsActive);
var html = await httpClient.GetStringAsync(auctionUrl);
if (!string.IsNullOrEmpty(productKey))
{
query = query.Where(i => i.ProductKey == productKey);
}
return await query.OrderByDescending(i => i.ConfidenceLevel).ToListAsync();
Console.WriteLine($"[StatsService] HTML scaricato ({html.Length} chars), parsing...");
// Usa il metodo esistente di ClosedAuctionsScraper per estrarre le puntate
var bidsUsed = ExtractBidsUsedFromHtml(html);
return bidsUsed;
}
catch (TaskCanceledException)
{
Console.WriteLine($"[StatsService] ? Timeout durante download HTML (>5s) - URL: {auctionUrl}");
return null;
}
catch (HttpRequestException ex)
{
Console.WriteLine($"[StatsService] ? Errore HTTP durante scraping: {ex.Message}");
return null;
}
catch (Exception ex)
{
Console.WriteLine($"[PostgreSQL ERROR] Failed to get insights: {ex.Message}");
return new List<StrategicInsight>();
Console.WriteLine($"[StatsService] ? Errore generico durante scraping: {ex.Message}");
return null;
}
}
/// <summary>
/// Ottiene performance puntatori da PostgreSQL
/// Estrae le puntate usate dall'HTML (stesso algoritmo di ClosedAuctionsScraper)
/// </summary>
public async Task<List<BidderPerformance>> GetTopCompetitorsAsync(int limit = 10)
private int? ExtractBidsUsedFromHtml(string html)
{
if (!_postgresAvailable || _postgresDb == null)
if (string.IsNullOrEmpty(html)) return null;
// 1) Look for the explicit bids-used span: <p ...><span>628</span> Puntate utilizzate</p>
var match = System.Text.RegularExpressions.Regex.Match(html,
"class=\\\"bids-used\\\"[^>]*>[^<]*<span[^>]*>(?<n>[0-9]{1,7})</span>",
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
if (match.Success && int.TryParse(match.Groups["n"].Value, out var val1))
{
return new List<BidderPerformance>();
Console.WriteLine($"[StatsService] Puntate estratte (metodo 1 - bids-used class): {val1}");
return val1;
}
// 2) Look for numeric followed by 'Puntate utilizzate' or similar
match = System.Text.RegularExpressions.Regex.Match(html,
"(?<n>[0-9]{1,7})\\s*(?:Puntate utilizzate|Puntate usate|puntate utilizzate|puntate usate|puntate)\\b",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
if (match.Success && int.TryParse(match.Groups["n"].Value, out var val2))
{
Console.WriteLine($"[StatsService] Puntate estratte (metodo 2 - pattern testo): {val2}");
return val2;
}
// 3) Fallbacks
match = System.Text.RegularExpressions.Regex.Match(html,
"(?<n>[0-9]+)\\s*(?:puntate|Puntate|puntate usate|puntate_usate|pt\\.?|pts)\\b",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
if (match.Success && int.TryParse(match.Groups["n"].Value, out var val3))
{
Console.WriteLine($"[StatsService] Puntate estratte (metodo 3 - fallback): {val3}");
return val3;
}
Console.WriteLine($"[StatsService] Nessun pattern trovato per le puntate nell'HTML");
return null;
}
/// <summary>
/// Registra il completamento di un'asta (overload semplificato per compatibilità)
/// </summary>
public async Task RecordAuctionCompletedAsync(AuctionInfo auction, bool won)
{
if (auction.LastState != null)
{
await RecordAuctionCompletedAsync(auction, auction.LastState, won);
}
else
{
Console.WriteLine("[StatsService] Cannot record auction - LastState is null");
}
}
/// <summary>
/// Ottiene i limiti consigliati per un prodotto
/// </summary>
public async Task<RecommendedLimits?> GetRecommendedLimitsAsync(string productName)
{
if (_productStatsService == null) return null;
var productKey = ProductStatisticsService.GenerateProductKey(productName);
return await _productStatsService.GetRecommendedLimitsAsync(productKey);
}
/// <summary>
/// Ottiene le statistiche di un singolo prodotto
/// </summary>
public ProductStatisticsRecord? GetProductStats(string productKey)
{
if (_productStatsService == null || !IsAvailable) return null;
try
{
return await _postgresDb.BidderPerformances
.OrderByDescending(b => b.WinRate)
.Take(limit)
.ToListAsync();
// Carica statistiche dal database in modo sincrono
var allStats = _productStatsService.GetAllProductStatisticsAsync().GetAwaiter().GetResult();
return allStats.FirstOrDefault(p => p.ProductKey == productKey);
}
catch (Exception ex)
catch
{
Console.WriteLine($"[PostgreSQL ERROR] Failed to get competitors: {ex.Message}");
return new List<BidderPerformance>();
return null;
}
}
/// <summary>
/// Ottiene tutte le statistiche prodotto
/// </summary>
public async Task<List<ProductStatisticsRecord>> GetAllProductStatisticsAsync()
{
if (_productStatsService == null) return new List<ProductStatisticsRecord>();
return await _productStatsService.GetAllProductStatisticsAsync();
}
// Metodi esistenti per compatibilità SQLite
// Metodi per query statistiche
public async Task<List<DailyStat>> GetDailyStatsAsync(int days = 30)
{
if (!IsAvailable)
{
return new List<DailyStat>();
}
var to = DateTime.UtcNow;
var from = to.AddDays(-days);
return await _db.GetDailyStatsAsync(from, to);
@@ -319,6 +361,11 @@ namespace AutoBidder.Services
public async Task<TotalStats> GetTotalStatsAsync()
{
if (!IsAvailable)
{
return new TotalStats();
}
var stats = await GetDailyStatsAsync(365);
return new TotalStats
@@ -338,13 +385,23 @@ namespace AutoBidder.Services
};
}
public async Task<List<AuctionResult>> GetRecentAuctionResultsAsync(int limit = 50)
public async Task<List<AuctionResultExtended>> GetRecentAuctionResultsAsync(int limit = 50)
{
if (!IsAvailable)
{
return new List<AuctionResultExtended>();
}
return await _db.GetRecentAuctionResultsAsync(limit);
}
public async Task<double> CalculateROIAsync()
{
if (!IsAvailable)
{
return 0;
}
var stats = await GetTotalStatsAsync();
if (stats.TotalMoneySpent <= 0)
@@ -355,11 +412,22 @@ namespace AutoBidder.Services
public async Task<ChartData> GetChartDataAsync(int days = 30)
{
if (!IsAvailable)
{
return new ChartData
{
Labels = new List<string>(),
MoneySpent = new List<double>(),
Savings = new List<double>()
};
}
var stats = await GetDailyStatsAsync(days);
var allDates = new List<DailyStat>();
var startDate = DateTime.UtcNow.AddDays(-days);
for (int i = 0; i < days; i++)
{
var date = startDate.AddDays(i).ToString("yyyy-MM-dd");
@@ -387,11 +455,6 @@ namespace AutoBidder.Services
Savings = allDates.Select(s => s.TotalSavings).ToList()
};
}
/// <summary>
/// Indica se il database PostgreSQL è disponibile
/// </summary>
public bool IsPostgresAvailable => _postgresAvailable;
}
// Classi esistenti per compatibilità
+20
View File
@@ -0,0 +1,20 @@
@inherits LayoutComponentBase
<div class="login-page">
@Body
</div>
<style>
.login-page {
/* Layout fullscreen per pagina login */
min-height: 100vh;
width: 100vw;
overflow: hidden;
}
/* Nascondi sidebar se presente */
.login-page + .sidebar,
.login-page .sidebar {
display: none !important;
}
</style>
+92 -15
View File
@@ -1,26 +1,103 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<div class="app-container">
<aside class="app-sidebar">
<NavMenu />
</div>
</aside>
<main>
<!-- UserBanner rimosso - informazioni integrate nel toolbar dell'Index.razor -->
<article class="content">
<main class="app-main">
<article class="app-content">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
<environment include="Staging,Production">
Si è verificato un errore.
</environment>
<environment include="Development">
Si è verificato un errore non gestito. Consultare la console del browser per ulteriori informazioni.
</environment>
<a href="" class="reload">Ricarica</a>
<a class="dismiss">??</a>
<div class="error-content">
<i class="bi bi-exclamation-triangle-fill"></i>
<span>Si e verificato un errore. <a href="" class="reload">Ricarica</a></span>
<button class="dismiss-btn" onclick="this.parentElement.parentElement.style.display='none'">×</button>
</div>
</div>
<style>
.app-container {
display: flex;
min-height: 100vh;
background: #0f0f0f;
}
.app-sidebar {
width: 260px;
min-width: 260px;
height: 100vh;
position: fixed;
left: 0;
top: 0;
z-index: 1000;
}
.app-main {
flex: 1;
margin-left: 260px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.app-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
}
#blazor-error-ui {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
}
#blazor-error-ui .error-content {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.5rem;
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
color: white;
font-weight: 500;
}
#blazor-error-ui .reload {
color: white;
text-decoration: underline;
}
#blazor-error-ui .dismiss-btn {
margin-left: auto;
background: none;
border: none;
color: white;
font-size: 1.5rem;
cursor: pointer;
opacity: 0.8;
}
#blazor-error-ui .dismiss-btn:hover {
opacity: 1;
}
@@media (max-width: 768px) {
.app-sidebar {
width: 100%;
height: auto;
position: relative;
}
.app-main {
margin-left: 0;
}
}
</style>
+283 -87
View File
@@ -1,105 +1,301 @@
@using Microsoft.AspNetCore.Components.Authorization
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject AuctionMonitor AuctionMonitor
@implements IDisposable
<div class="sidebar">
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand d-flex align-items-center" href="">
<i class="bi bi-lightning-charge-fill me-2" style="font-size: 1.5rem; color: #ffc107;"></i>
<span class="fw-bold">AutoBidder</span>
</a>
<div class="nav-sidebar">
<div class="nav-header">
<a class="nav-brand" href="">
<div class="brand-icon">
<i class="bi bi-lightning-charge-fill"></i>
</div>
<span class="brand-text">AutoBidder</span>
</a>
</div>
<nav class="nav-menu">
<div class="nav-section">
<NavLink class="nav-menu-item" href="" Match="NavLinkMatch.All">
<i class="bi bi-display"></i>
<span>Monitor Aste</span>
</NavLink>
<NavLink class="nav-menu-item" href="browser">
<i class="bi bi-search"></i>
<span>Esplora Aste</span>
</NavLink>
<NavLink class="nav-menu-item" href="statistics">
<i class="bi bi-bar-chart"></i>
<span>Statistiche</span>
</NavLink>
<NavLink class="nav-menu-item" href="settings">
<i class="bi bi-gear"></i>
<span>Impostazioni</span>
</NavLink>
</div>
</div>
<div class="nav-scrollable">
<nav class="flex-column px-3 mt-3">
<div class="nav-item px-2 mb-2 animate-fade-in-left stagger-item">
<NavLink class="nav-link hover-lift transition-all" href="" Match="NavLinkMatch.All">
<i class="bi bi-display me-2"></i> Monitor Aste
</NavLink>
</div>
<div class="nav-item px-2 mb-2 animate-fade-in-left stagger-item">
<NavLink class="nav-link hover-lift transition-all" href="freebids">
<i class="bi bi-gift me-2"></i> Puntate Gratuite
</NavLink>
</div>
<div class="nav-item px-2 mb-2 animate-fade-in-left stagger-item">
<NavLink class="nav-link hover-lift transition-all" href="statistics">
<i class="bi bi-bar-chart me-2"></i> Statistiche
</NavLink>
</div>
<div class="nav-item px-2 mb-2 animate-fade-in-left stagger-item">
<NavLink class="nav-link hover-lift transition-all" href="settings">
<i class="bi bi-gear me-2"></i> Impostazioni
</NavLink>
</div>
</nav>
</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 @(string.IsNullOrEmpty(sessionUsername) ? "disconnected" : "connected")">
<i class="bi bi-person-circle"></i>
<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>
<span>Logout</span>
</a>
</Authorized>
</AuthorizeView>
</div>
</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>
.sidebar {
.nav-sidebar {
display: flex;
flex-direction: column;
}
.navbar-brand {
font-size: 1.3rem;
transition: all 0.3s ease;
color: white !important;
}
.navbar-brand:hover {
transform: scale(1.05);
text-shadow: 0 0 10px rgba(255, 193, 7, 0.5);
}
.nav-scrollable {
flex: 1;
overflow-y: auto;
padding-bottom: 2rem;
}
.nav-link {
border-radius: 8px;
margin: 0.25rem 0;
padding: 0.75rem 1rem;
font-weight: 500;
position: relative;
overflow: hidden;
color: rgba(255, 255, 255, 0.8) !important;
transition: all 0.3s ease;
}
.nav-link::before {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 4px;
background: linear-gradient(to bottom, #0dcaf0, #0d6efd);
transform: scaleY(0);
transition: transform 0.3s ease;
background: linear-gradient(180deg, #1a1d23 0%, #13151a 100%);
border-right: 1px solid rgba(255, 255, 255, 0.06);
}
.nav-link:hover {
background: rgba(255, 255, 255, 0.1);
color: white !important;
.nav-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.nav-link:hover::before,
.nav-link.active::before {
transform: scaleY(1);
.nav-brand {
display: flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
transition: opacity 0.2s;
}
.nav-link.active {
background: linear-gradient(to right, rgba(13, 202, 240, 0.2), transparent);
font-weight: 600;
color: #0dcaf0 !important;
.nav-brand:hover {
opacity: 0.9;
}
.brand-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border-radius: 10px;
font-size: 1.25rem;
color: white;
}
.brand-text {
font-size: 1.25rem;
font-weight: 700;
color: white;
letter-spacing: -0.02em;
}
.nav-menu {
display: flex;
flex-direction: column;
flex: 1;
padding: 1rem 0.75rem;
overflow-y: auto;
}
.nav-section {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.nav-menu-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 8px;
color: rgba(255, 255, 255, 0.65);
text-decoration: none;
font-size: 0.9375rem;
font-weight: 500;
transition: all 0.15s ease;
}
.nav-menu-item i {
font-size: 1.125rem;
width: 1.5rem;
text-align: center;
}
.nav-menu-item:hover {
background: rgba(255, 255, 255, 0.06);
color: white;
}
.nav-menu-item.active {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%);
color: #a5b4fc;
}
.nav-menu-item.active i {
color: #818cf8;
}
.nav-footer {
padding: 1rem;
margin-top: auto;
padding-top: 1rem;
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;
gap: 0.75rem;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 8px;
color: rgba(255, 255, 255, 0.5);
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;
}
.logout-item {
color: rgba(248, 113, 113, 0.8) !important;
}
.logout-item:hover {
background: rgba(248, 113, 113, 0.1) !important;
color: #f87171 !important;
}
</style>
+26
View File
@@ -0,0 +1,26 @@
@using Microsoft.AspNetCore.Components
@inject NavigationManager Navigation
<div class="d-flex justify-content-center align-items-center" style="min-height: 100vh; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Caricamento...</span>
</div>
</div>
@code {
private bool _hasRedirected = false;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !_hasRedirected)
{
_hasRedirected = true;
// Redirect semplice senza returnUrl per evitare problemi
Navigation.NavigateTo("/Account/Login", forceLoad: true);
}
await base.OnAfterRenderAsync(firstRender);
}
}
+15 -3
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;
}
+389 -21
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;
@@ -71,44 +98,378 @@ namespace AutoBidder.Utilities
/// </summary>
public string MinLogLevel { get; set; } = "Normal";
// CONFIGURAZIONE DATABASE POSTGRESQL
/// <summary>
/// Abilita l'uso di PostgreSQL per statistiche avanzate
/// </summary>
public bool UsePostgreSQL { get; set; } = true;
// ???????????????????????????????????????????????????????????????
// IMPOSTAZIONI DATABASE
// ???????????????????????????????????????????????????????????????
/// <summary>
/// Connection string PostgreSQL
/// Abilita il salvataggio automatico delle aste completate nel database.
/// Default: true (consigliato per statistiche)
/// </summary>
public string PostgresConnectionString { get; set; } = "Host=localhost;Port=5432;Database=autobidder_stats;Username=autobidder;Password=autobidder_password";
public bool DatabaseAutoSaveEnabled { get; set; } = true;
/// <summary>
/// Auto-crea schema database se mancante
/// Esegue pulizia automatica duplicati all'avvio dell'applicazione.
/// Default: true (consigliato per mantenere database pulito)
/// </summary>
public bool AutoCreateDatabaseSchema { get; set; } = true;
public bool DatabaseAutoCleanupDuplicates { get; set; } = true;
/// <summary>
/// Fallback automatico a SQLite se PostgreSQL non disponibile
/// Esegue pulizia automatica record incompleti all'avvio.
/// Default: false (può rimuovere dati utili in caso di errori temporanei)
/// </summary>
public bool FallbackToSQLite { get; set; } = true;
public bool DatabaseAutoCleanupIncomplete { get; set; } = false;
/// <summary>
/// Numero massimo di giorni da mantenere nei risultati aste.
/// 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>
/// Scelta priorità limiti quando si aggiunge un'asta per un prodotto già salvato:
/// - "ProductStats": Usa i limiti personalizzati salvati nelle statistiche prodotto (UserDefaultMinPrice, ecc.)
/// - "GlobalDefaults": Usa sempre i limiti globali (DefaultMinPrice, DefaultMaxPrice, ecc.)
/// Default: "ProductStats" (consigliato per usare limiti specifici per prodotto)
/// </summary>
public string NewAuctionLimitsPriority { get; set; } = "ProductStats";
/// <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
{
private static readonly string _folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AutoBidder");
private static readonly string _file = Path.Combine(_folder, "settings.json");
// Cache per evitare letture disco ripetute nel hot path (ticker loop 50ms)
private static readonly object _cacheLock = new();
private static AppSettings? _cached;
private static DateTime _cacheExpiry = DateTime.MinValue;
private const int CACHE_TTL_MS = 2000; // Ricarica da disco al massimo ogni 2s
public static AppSettings Load()
{
try
lock (_cacheLock)
{
if (!File.Exists(_file)) return new AppSettings();
var txt = File.ReadAllText(_file);
var s = JsonSerializer.Deserialize<AppSettings>(txt);
if (s == null) return new AppSettings();
return s;
var now = DateTime.UtcNow;
if (_cached != null && now < _cacheExpiry)
return _cached;
try
{
if (!File.Exists(_file))
{
_cached = new AppSettings();
}
else
{
var txt = File.ReadAllText(_file);
_cached = JsonSerializer.Deserialize<AppSettings>(txt) ?? new AppSettings();
}
}
catch
{
_cached ??= new AppSettings();
}
_cacheExpiry = now.AddMilliseconds(CACHE_TTL_MS);
return _cached;
}
catch { return new AppSettings(); }
}
public static void Save(AppSettings settings)
@@ -118,6 +479,13 @@ namespace AutoBidder.Utilities
if (!Directory.Exists(_folder)) Directory.CreateDirectory(_folder);
var txt = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_file, txt);
// Invalida cache così il prossimo Load() legge i nuovi valori
lock (_cacheLock)
{
_cached = settings;
_cacheExpiry = DateTime.UtcNow.AddMilliseconds(CACHE_TTL_MS);
}
}
catch { }
}
-183
View File
@@ -1,183 +0,0 @@
# ? Verifica Configurazione Docker + Gitea (2026)
## ?? Checklist Completa secondo Guida Gitea
### ? 1. Preparazione su Gitea
| Requisito | Stato | Dettagli |
|-----------|-------|----------|
| Container Registry abilitato | ? CONFERMATO | Package esistente su `https://gitea.encke-hake.ts.net/Alby96/-/packages` |
| Token PAT generato | ?? DA VERIFICARE | Deve avere permessi `read:packages` + `write:packages` |
| Token usato per login | ?? DA FARE | `docker login gitea.encke-hake.ts.net` con Token come password |
**?? AZIONE RICHIESTA:**
```bash
# Genera token su: https://gitea.encke-hake.ts.net/user/settings/applications
# Scope necessari: read:packages, write:packages
# Poi autentica Docker:
docker login gitea.encke-hake.ts.net
# Username: Alby96
# Password: [TOKEN_PAT_GENERATO]
```
---
### ? 2. Configurazione Progetto Visual Studio
| Requisito | Stato | Dettagli |
|-----------|-------|----------|
| Supporto Docker abilitato | ? OK | `DockerDefaultTargetOS=Linux`, `DockerfileFile=Dockerfile` |
| Dockerfile presente | ? OK | Valido, espone porta 8080, healthcheck configurato |
| .dockerignore presente | ? OK | Esclude file non necessari |
| Profili pubblicazione | ? OK | `GiteaRegistry.pubxml` e `GiteaRegistry-Versioned.pubxml` |
---
### ?? 3. Convenzione Nomi Docker (CORRETTO)
**? PROBLEMA RILEVATO E CORRETTO:**
**Prima (ERRATO - 4 livelli):**
```
gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
??????????????????????? ?????? ??????? ???????????
registro owner ??? ??? immagine
```
**Dopo correzione (CORRETTO - 3 livelli):**
```
gitea.encke-hake.ts.net/alby96/autobidder:latest
??????????????????????? ?????? ???????????
registro owner immagine
```
**?? Convenzione Gitea ufficiale:**
```
Sintassi: {registro}/{proprietario}/{immagine}:{tag}
Esempio: gitea.example.com/mio-utente/mia-app:latest
```
**? MODIFICHE APPLICATE:**
- `AutoBidder.csproj`: `<ContainerRegistry>gitea.encke-hake.ts.net/alby96</ContainerRegistry>`
- `GiteaRegistry.pubxml`: Aggiornato con path corretto
- `DOCKER_PUBLISH_GUIDE.md`: Tutti i comandi aggiornati
---
### ? 4. File Modificati
#### `AutoBidder.csproj`
```xml
<!-- Metadata immagine Docker -->
<ContainerImageName>autobidder</ContainerImageName>
<ContainerImageTag>$(Version)</ContainerImageTag>
<!-- CORRETTO: Convenzione Gitea {registro}/{proprietario}/{immagine} -->
<ContainerRegistry>gitea.encke-hake.ts.net/alby96</ContainerRegistry>
```
#### `Properties/PublishProfiles/GiteaRegistry.pubxml`
```xml
<!-- CORRETTO: {registro}/{proprietario} senza livelli extra -->
<ContainerRegistry>gitea.encke-hake.ts.net/alby96</ContainerRegistry>
<ContainerImageName>autobidder</ContainerImageName>
```
#### `Dockerfile`
? **Nessuna modifica necessaria** - Il Dockerfile è corretto:
- Build multi-stage (sdk ? publish ? runtime)
- Porta 8080 esposta
- Healthcheck configurato
- Labels OCI
- Variabili ambiente corrette
---
## ?? Procedura di Test
### 1. Autenticazione
```bash
docker logout gitea.encke-hake.ts.net
docker login gitea.encke-hake.ts.net
# Username: Alby96
# Password: [TOKEN_PAT]
```
### 2. Build con Convenzione Corretta
```bash
# Rebuild completo senza cache
docker build --no-cache \
-t gitea.encke-hake.ts.net/alby96/autobidder:latest \
-t gitea.encke-hake.ts.net/alby96/autobidder:1.0.0 \
.
```
### 3. Push su Gitea
```bash
docker push gitea.encke-hake.ts.net/alby96/autobidder:latest
docker push gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
```
### 4. Verifica su Gitea
```
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder/latest
```
Dovresti vedere:
- Nome package: `autobidder` (NON più `mimante/autobidder`)
- Tag disponibili: `latest`, `1.0.0`
- Data aggiornata ad oggi
- Digest SHA256 nuovo
---
## ?? Confronto Prima/Dopo
| Aspetto | Prima (Errato) | Dopo (Corretto) |
|---------|----------------|-----------------|
| **Path Registry** | `gitea.encke-hake.ts.net/alby96/mimante` | `gitea.encke-hake.ts.net/alby96` |
| **Immagine Completa** | `gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest` | `gitea.encke-hake.ts.net/alby96/autobidder:latest` |
| **Package su Gitea** | `mimante/autobidder` | `autobidder` |
| **Link Gitea** | `.../container/mimante%2Fautobidder/...` | `.../container/autobidder/...` |
| **Livelli Path** | 4 (errato) | 3 (corretto) |
| **Conforme Guida Gitea** | ? NO | ? SÌ |
---
## ?? Possibili Problemi e Soluzioni
### Problema 1: Package vecchio ancora visibile
**Soluzione:** Il vecchio package `mimante/autobidder` continuerà ad esistere. Puoi:
- Eliminarlo manualmente da Gitea (Settings del package)
- Oppure lasciarlo (non interferisce con il nuovo)
### Problema 2: Autenticazione fallita
**Soluzione:**
- Usa Token PAT invece della password
- Verifica scope del token: `read:packages`, `write:packages`
- Se hai 2FA attivo, il Token è OBBLIGATORIO
### Problema 3: SSL/TLS Errors
**Soluzione:** Se Gitea usa certificati self-signed:
```bash
# Aggiungi a Docker daemon.json
{
"insecure-registries": ["gitea.encke-hake.ts.net"]
}
```
---
## ? Configurazione Finale Verificata
**Tutti i requisiti soddisfatti:**
- ? Container Registry Gitea abilitato
- ? Dockerfile corretto e ottimizzato
- ? Convenzione nomi corretta (3 livelli)
- ? Profili di pubblicazione aggiornati
- ? Supporto Docker in Visual Studio
- ? Build multi-stage funzionante
- ? Healthcheck configurato
- ?? Token PAT da generare/verificare
**Prossimo step:** Genera Token PAT e testa il push!
-305
View File
@@ -1,305 +0,0 @@
# ?? Sistema di Versionamento Automatico
## ?? Strategia Versioning
Il progetto AutoBidder segue **[Semantic Versioning 2.0.0](https://semver.org/)** nel formato:
```
MAJOR.MINOR.PATCH
```
### Quando Incrementare
| Tipo | Quando | Esempio |
|------|--------|---------|
| **MAJOR** | Breaking changes | `1.5.2` ? `2.0.0` |
| **MINOR** | Nuove feature retrocompatibili | `1.5.2` ? `1.6.0` |
| **PATCH** | Bug fix retrocompatibili | `1.5.2` ? `1.5.3` |
---
## ?? Workflow di Rilascio
### 1. Modifica Versione in `.csproj`
```xml
<!-- AutoBidder.csproj -->
<PropertyGroup>
<!-- v1.1.0: Docker/Gitea publishing workflow + HTTPS fix -->
<Version>1.1.0</Version>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0</InformationalVersion>
</PropertyGroup>
```
**? Questa è la FONTE UNICA della versione!**
### 2. Aggiorna `Dockerfile` Labels
```dockerfile
LABEL org.opencontainers.image.version="1.1.0"
```
### 3. Documenta in `CHANGELOG.md`
```markdown
## [1.1.0] - 2025-01-18
### ? Aggiunte
- Pubblicazione automatica su Gitea
- ...
### ?? Modifiche
- Porta HTTP: 5000 ? 8080
- ...
```
### 4. Pubblica su Gitea
```bash
# Da Visual Studio: Tasto destro ? Pubblica ? GiteaRegistry
# Oppure da CLI:
dotnet publish /p:PublishProfile=GiteaRegistry
```
**Risultato automatico:**
- `gitea.../autobidder:latest` (aggiornato)
- `gitea.../autobidder:1.1.0` (nuovo tag)
---
## ?? Storico Versioni
### v1.1.0 - Docker/Gitea Publishing Workflow (2025-01-18)
**Feature Principali:**
- ? Pubblicazione automatica Gitea Container Registry
- ? Versionamento automatico da `.csproj`
- ?? HTTPS disabilitato di default in container
- ?? Porta HTTP standardizzata (8080)
- ?? Fix errore Visual Studio "ContainerBuild"
- ?? Fix crash container certificati HTTPS
**Breaking Changes:**
- ?? Porta: `5000` ? `8080`
- ?? Path Gitea: `alby96/mimante/autobidder` ? `alby96/autobidder`
- ?? HTTPS: abilitato ? disabilitato (opzionale)
**Migrazione:**
```bash
# Aggiorna port mapping
docker run -p 5000:8080 ... # era 5000:5000
# Pull nuova convenzione path
docker pull gitea.../alby96/autobidder:1.1.0
```
### v1.0.0 - Release Iniziale (2025-01-17)
**Feature Principali:**
- ? Sistema AutoBidder Blazor .NET 8
- ? Monitoraggio aste Bidoo
- ? Offerte automatiche
- ? Statistiche PostgreSQL
- ? Docker support base
---
## ?? Esempi Pratici
### Scenario 1: Bug Fix
**Situazione:** Corretto bug calcolo statistiche
```xml
<!-- Prima -->
<Version>1.1.0</Version>
<!-- Dopo -->
<Version>1.1.1</Version>
```
```markdown
## [1.1.1] - 2025-01-19
### ?? Correzioni
- Fix calcolo media offerte in Statistics.razor
```
### Scenario 2: Nuova Feature
**Situazione:** Aggiunto supporto notifiche email
```xml
<!-- Prima -->
<Version>1.1.1</Version>
<!-- Dopo -->
<Version>1.2.0</Version>
```
```markdown
## [1.2.0] - 2025-01-20
### ? Aggiunte
- Notifiche email per aste vinte
- Configurazione SMTP in Settings
```
### Scenario 3: Breaking Change
**Situazione:** API REST completamente ristrutturata
```xml
<!-- Prima -->
<Version>1.2.0</Version>
<!-- Dopo -->
<Version>2.0.0</Version>
```
```markdown
## [2.0.0] - 2025-02-01
### ?? BREAKING CHANGES
- API REST ristrutturata (endpoints modificati)
- Migrazione richiesta per client esistenti
### ?? Modifiche
- Endpoint `/api/auctions` ? `/api/v2/auctions`
- Response format JSON standardizzato
```
---
## ?? Automazione
### GitHub Actions / Gitea Actions
```yaml
# .gitea/workflows/version-check.yml
name: Version Check
on:
push:
branches: [ main, docker ]
jobs:
check-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Extract version
id: version
run: |
VERSION=$(grep -oP '<Version>\K[^<]+' AutoBidder.csproj | head -1)
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Check CHANGELOG
run: |
if ! grep -q "## \[${{ steps.version.outputs.version }}\]" CHANGELOG.md; then
echo "?? Versione ${{ steps.version.outputs.version }} non documentata in CHANGELOG.md"
exit 1
fi
- name: Create Git Tag
run: |
git tag v${{ steps.version.outputs.version }}
git push origin v${{ steps.version.outputs.version }}
```
### PowerShell Script Locale
```powershell
# scripts/bump-version.ps1
param(
[Parameter(Mandatory=$true)]
[ValidateSet('major','minor','patch')]
[string]$Type
)
# Leggi versione corrente
$csproj = "AutoBidder.csproj"
$content = Get-Content $csproj -Raw
$version = [regex]::Match($content, '<Version>(.*?)</Version>').Groups[1].Value
# Parse semantic version
$parts = $version -split '\.'
$major = [int]$parts[0]
$minor = [int]$parts[1]
$patch = [int]$parts[2]
# Incrementa
switch ($Type) {
'major' { $major++; $minor=0; $patch=0 }
'minor' { $minor++; $patch=0 }
'patch' { $patch++ }
}
$newVersion = "$major.$minor.$patch"
# Aggiorna .csproj
$content = $content -replace '<Version>.*?</Version>', "<Version>$newVersion</Version>"
$content = $content -replace '<AssemblyVersion>.*?</AssemblyVersion>', "<AssemblyVersion>$newVersion.0</AssemblyVersion>"
$content = $content -replace '<FileVersion>.*?</FileVersion>', "<FileVersion>$newVersion.0</FileVersion>"
$content = $content -replace '<InformationalVersion>.*?</InformationalVersion>', "<InformationalVersion>$newVersion</InformationalVersion>"
Set-Content $csproj $content
# Aggiorna Dockerfile
$dockerfile = "Dockerfile"
$dockerContent = Get-Content $dockerfile -Raw
$dockerContent = $dockerContent -replace 'org.opencontainers.image.version=".*?"', "org.opencontainers.image.version=""$newVersion"""
Set-Content $dockerfile $dockerContent
Write-Host "? Versione aggiornata: $version ? $newVersion"
Write-Host "?? Ricorda di aggiornare CHANGELOG.md!"
```
**Uso:**
```powershell
# Incrementa PATCH (bug fix)
.\scripts\bump-version.ps1 -Type patch
# Incrementa MINOR (nuova feature)
.\scripts\bump-version.ps1 -Type minor
# Incrementa MAJOR (breaking change)
.\scripts\bump-version.ps1 -Type major
```
---
## ?? Riferimenti
- [Semantic Versioning 2.0.0](https://semver.org/)
- [Keep a Changelog](https://keepachangelog.com/)
- [Conventional Commits](https://www.conventionalcommits.org/)
- [GitVersion](https://gitversion.net/) (tool automatico)
---
## ? Checklist Release
Prima di ogni release:
- [ ] Versione incrementata in `AutoBidder.csproj`
- [ ] Versione aggiornata in `Dockerfile` labels
- [ ] Modifiche documentate in `CHANGELOG.md`
- [ ] Build locale testata
- [ ] Container Docker testato localmente
- [ ] Pubblicazione su Gitea completata
- [ ] Tag Git creato (`v1.1.0`)
- [ ] Documentazione aggiornata (se necessario)
**Dopo la release:**
- [ ] Verifica immagine su Gitea
- [ ] Test pull e deploy
- [ ] Comunicazione team (se applicabile)
- [ ] Aggiornamento deployment production
---
**?? Versione corrente:** `1.1.0` - Docker/Gitea Publishing Workflow
-340
View File
@@ -1,340 +0,0 @@
# ?? SISTEMA VERSIONAMENTO IMPLEMENTATO
## ? Versione Corrente: `1.1.0`
**Data:** 2025-01-18
**Tipo:** MINOR (nuove feature + bug fix)
**Modifiche:** Docker/Gitea Publishing Workflow + HTTPS Fix
---
## ?? File Creati/Aggiornati
### Nuovi File
1. **`CHANGELOG.md`**
- Storico completo modifiche
- Formato [Keep a Changelog](https://keepachangelog.com/)
- Documentazione v1.1.0 completa
2. **`VERSIONING.md`**
- Guida sistema versionamento
- Workflow di rilascio
- Esempi pratici
- Automazione
3. **`bump-version.ps1`**
- Script PowerShell automatico
- Incrementa MAJOR/MINOR/PATCH
- Aggiorna tutti i file coinvolti
- Genera template CHANGELOG
### File Aggiornati
1. **`AutoBidder.csproj`**
```xml
<!-- v1.1.0: Docker/Gitea publishing workflow + HTTPS fix -->
<Version>1.1.0</Version>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0</InformationalVersion>
```
2. **`Dockerfile`**
```dockerfile
LABEL org.opencontainers.image.version="1.1.0"
```
---
## ?? Come Usare il Sistema
### Metodo 1: Script Automatico (CONSIGLIATO)
```powershell
# Bug fix (1.1.0 ? 1.1.1)
.\bump-version.ps1 -Type patch
# Nuova feature (1.1.0 ? 1.2.0)
.\bump-version.ps1 -Type minor
# Breaking change (1.1.0 ? 2.0.0)
.\bump-version.ps1 -Type major
```
**Lo script fa automaticamente:**
1. ? Incrementa versione in `AutoBidder.csproj`
2. ? Aggiorna `Dockerfile` labels
3. ? Aggiunge template in `CHANGELOG.md`
4. ? Mostra prossimi passi
### Metodo 2: Manuale
1. **Modifica `AutoBidder.csproj`:**
```xml
<Version>1.2.0</Version>
```
2. **Modifica `Dockerfile`:**
```dockerfile
LABEL org.opencontainers.image.version="1.2.0"
```
3. **Aggiorna `CHANGELOG.md`:**
```markdown
## [1.2.0] - 2025-01-19
### ? Aggiunte
- Nuova feature X
```
4. **Pubblica:**
```bash
dotnet publish /p:PublishProfile=GiteaRegistry
```
---
## ?? Workflow Completo di Rilascio
### Step 1: Incrementa Versione
```powershell
.\bump-version.ps1 -Type minor
```
### Step 2: Compila CHANGELOG
Apri `CHANGELOG.md` e completa il template:
```markdown
## [1.2.0] - 2025-01-19
### ? Aggiunte
- Feature notifiche email per aste vinte
- Configurazione SMTP in Settings
### ?? Modifiche
- Migliorato algoritmo calcolo statistiche
### ?? Correzioni
- Fix bug crash su asta annullata
```
### Step 3: Commit Modifiche
```bash
git add AutoBidder.csproj Dockerfile CHANGELOG.md
git commit -m "chore: bump version to v1.2.0
- Feature notifiche email
- Fix bug crash asta annullata"
```
### Step 4: Tag Git
```bash
git tag v1.2.0
git push origin docker --tags
```
### Step 5: Pubblica Docker su Gitea
**Da Visual Studio:**
- Tasto destro ? Pubblica ? GiteaRegistry
**Da CLI:**
```bash
dotnet publish /p:PublishProfile=GiteaRegistry
```
### Step 6: Verifica Pubblicazione
```bash
# Controlla su Gitea
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder
# Verifica tag creati
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.2.0
```
---
## ?? Semantic Versioning
| Versione | Tipo | Quando Usare | Esempio |
|----------|------|--------------|---------|
| **1.0.0 ? 2.0.0** | MAJOR | Breaking changes | API cambiata, porta diversa |
| **1.0.0 ? 1.1.0** | MINOR | Nuove feature | Notifiche email, esportazione dati |
| **1.0.0 ? 1.0.1** | PATCH | Bug fix | Fix crash, correzione calcoli |
### Esempi Pratici
**Bug Fix (PATCH):**
```powershell
.\bump-version.ps1 -Type patch
# 1.1.0 ? 1.1.1
```
**Nuova Feature (MINOR):**
```powershell
.\bump-version.ps1 -Type minor
# 1.1.1 ? 1.2.0
```
**Breaking Change (MAJOR):**
```powershell
.\bump-version.ps1 -Type major
# 1.2.0 ? 2.0.0
```
---
## ?? Tag Docker Generati
### Dopo Pubblicazione v1.1.0
```bash
# Tag su Gitea
gitea.encke-hake.ts.net/alby96/autobidder:latest ? v1.1.0
gitea.encke-hake.ts.net/alby96/autobidder:1.1.0 ? immutabile
gitea.encke-hake.ts.net/alby96/autobidder:1.0.0 ? ancora disponibile
```
### Production Best Practice
**? NON USARE `latest` in production:**
```yaml
# ERRATO
image: gitea.../autobidder:latest
```
**? USA versione specifica:**
```yaml
# CORRETTO
image: gitea.../autobidder:1.1.0
```
**Motivo:** `latest` cambia ad ogni release, versione specifica è immutabile.
---
## ?? Gestione Hotfix
### Scenario: Bug critico in production
**Production usa:** `v1.1.0`
**Development è a:** `v1.2.0-dev`
**Workflow:**
1. **Crea branch hotfix:**
```bash
git checkout -b hotfix/1.1.1 v1.1.0
```
2. **Applica fix:**
```bash
# Fix bug
.\bump-version.ps1 -Type patch # 1.1.0 ? 1.1.1
```
3. **Pubblica hotfix:**
```bash
git commit -m "fix: critical bug in auction monitoring"
git tag v1.1.1
git push origin hotfix/1.1.1 --tags
dotnet publish /p:PublishProfile=GiteaRegistry
```
4. **Merge in main:**
```bash
git checkout docker
git merge hotfix/1.1.1
```
5. **Aggiorna development:**
```bash
# Se necessario, cherry-pick il fix in v1.2.0-dev
git cherry-pick <commit-hash>
```
---
## ?? Dashboard Versioni
### Versioni Attive
| Versione | Stato | Tag Docker | Ambiente |
|----------|-------|------------|----------|
| `1.1.0` | ? Latest | `latest`, `1.1.0` | Production |
| `1.0.0` | ?? Deprecated | `1.0.0` | Legacy |
### Roadmap
| Versione | Tipo | Piano | Data Target |
|----------|------|-------|-------------|
| `1.2.0` | MINOR | Notifiche email | Feb 2025 |
| `1.3.0` | MINOR | API REST | Mar 2025 |
| `2.0.0` | MAJOR | Refactor architettura | Q2 2025 |
---
## ? Checklist Release
Prima di ogni release:
- [ ] **Versione incrementata** in `AutoBidder.csproj`
- [ ] **Versione aggiornata** in `Dockerfile`
- [ ] **CHANGELOG.md** compilato con modifiche
- [ ] **Build locale** testata
- [ ] **Container Docker** testato localmente
- [ ] **Pubblicazione Gitea** completata
- [ ] **Tag Git** creato (`v1.1.0`)
- [ ] **Documentazione** aggiornata (se necessario)
- [ ] **Migration guide** scritta (per breaking changes)
- [ ] **Communication** team/utenti (se applicabile)
Dopo la release:
- [ ] **Verifica immagine** su Gitea
- [ ] **Test pull** e deploy
- [ ] **Monitoraggio** errori prime 24h
- [ ] **Aggiornamento** deployment production
---
## ?? Benefici del Sistema
### Prima (senza versioning)
- ? Versioni non tracciate
- ? Modifiche non documentate
- ? Impossibile rollback a versione specifica
- ? Difficile capire cosa è cambiato
### Dopo (con versioning)
- ? Ogni modifica tracciata con versione
- ? CHANGELOG completo e leggibile
- ? Rollback facile (`docker pull .../:1.0.0`)
- ? Deploy controllati e verificabili
- ? Automazione con script PowerShell
- ? Tag Docker immutabili per production
---
## ?? Documenti di Riferimento
| File | Scopo |
|------|-------|
| `CHANGELOG.md` | Storico modifiche per utenti |
| `VERSIONING.md` | Guida sistema per sviluppatori |
| `bump-version.ps1` | Automazione incremento versione |
| `AutoBidder.csproj` | Fonte unica della verità (versione) |
| `Dockerfile` | Metadata versione immagine |
---
**?? Versione attuale: `1.1.0` - Docker/Gitea Publishing Workflow**
**? Sistema di versionamento completamente implementato e operativo!**
+10 -43
View File
@@ -1,31 +1,6 @@
version: '3.8'
services:
# ================================================
# PostgreSQL Database (statistiche avanzate)
# ================================================
postgres:
image: postgres:16-alpine
container_name: autobidder-postgres
environment:
POSTGRES_DB: autobidder_stats
POSTGRES_USER: ${POSTGRES_USER:-autobidder}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-autobidder_password}
POSTGRES_INITDB_ARGS: --encoding=UTF8
volumes:
- postgres-data:/var/lib/postgresql/data
- ./postgres-backups:/backups
ports:
- "5432:5432"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-autobidder} -d autobidder_stats"]
interval: 10s
timeout: 5s
retries: 5
networks:
- autobidder-network
# ================================================
# AutoBidder Application
# ================================================
@@ -37,33 +12,29 @@ services:
BUILD_CONFIGURATION: Release
image: gitea.encke-hake.ts.net/alby96/autobidder:latest
container_name: autobidder
depends_on:
postgres:
condition: service_healthy
ports:
- "${APP_PORT:-5000}:8080" # Host:Container (HTTP only)
volumes:
# Persistent data (SQLite, backups, logs)
# Persistent data (SQLite databases, backups, logs, keys)
# Tutti i dati persistenti sono salvati in questo volume
- ./Data:/app/Data
# PostgreSQL backups
- ./postgres-backups:/app/Data/backups
environment:
# ASP.NET Core
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8080
# PostgreSQL connection
- ConnectionStrings__PostgreSQL=Host=postgres;Port=5432;Database=autobidder_stats;Username=${POSTGRES_USER:-autobidder};Password=${POSTGRES_PASSWORD:-autobidder_password}
# ============================================
# DATABASE PATH - Volume persistente Docker
# ============================================
# Tutti i database SQLite e dati persistenti usano questo path
- DATA_PATH=/app/Data
# Database settings
- Database__UsePostgres=${USE_POSTGRES:-true}
- Database__AutoCreateSchema=true
- Database__FallbackToSQLite=true
# Autenticazione applicazione (SICUREZZA)
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
# Logging
- Logging__LogLevel__Default=${LOG_LEVEL:-Information}
- Logging__LogLevel__Microsoft.EntityFrameworkCore=Warning
# Timezone
- TZ=Europe/Rome
@@ -77,10 +48,6 @@ services:
networks:
- autobidder-network
volumes:
postgres-data:
driver: local
networks:
autobidder-network:
driver: bridge
+12 -7
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 {
+18 -6
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 {
+365 -96
View File
@@ -1,68 +1,271 @@
/* app-wpf.css - WPF Dark Theme + Modern Sidebar */
/* app-wpf.css - Modern Dark Theme */
:root {
/* WPF Dark Theme Palette */
--bg-primary: #1e1e1e;
--bg-secondary: #252526;
--bg-tertiary: #2d2d30;
--bg-hover: #3e3e42;
--bg-selected: #094771;
--border-color: #3e3e42;
--text-primary: #ffffff;
--text-secondary: #cccccc;
--text-muted: #808080;
/* Modern Dark Palette */
--bg-primary: #0f0f0f;
--bg-secondary: #171717;
--bg-tertiary: #1f1f1f;
--bg-card: #1a1a1a;
--bg-hover: #262626;
--bg-selected: #2d2d2d;
--border-color: rgba(255, 255, 255, 0.08);
--border-subtle: rgba(255, 255, 255, 0.04);
/* WPF Accent Colors */
--primary-color: #007acc;
--success-color: #00d800;
--warning-color: #ffb700;
--danger-color: #e81123;
--info-color: #00b7c3;
/* Text Colors */
--text-primary: #fafafa;
--text-secondary: #a1a1aa;
--text-muted: #71717a;
/* Log Syntax Colors */
--log-success: #00d800;
--log-warning: #ffb700;
--log-error: #f48771;
--log-info: #4ec9b0;
--log-debug: #569cd6;
--log-timestamp: #808080;
/* Accent Colors */
--primary: #6366f1;
--primary-hover: #4f46e5;
--success: #22c55e;
--warning: #f59e0b;
--danger: #ef4444;
--info: #3b82f6;
/* Gradients */
--gradient-primary: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
--gradient-success: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
--gradient-danger: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.4);
/* Border Radius */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-xl: 20px;
}
/* === GLOBAL === */
* {
/* === GLOBAL RESET === */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
background: var(--bg-primary);
color: var(--text-secondary);
font-size: 13px;
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/* === LAYOUT === */
/* === SCROLLBAR === */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.15);
}
/* === LAYOUT (legacy support) === */
.page {
display: flex;
height: 100vh;
overflow: hidden;
}
/* Sidebar Moderna - 250px come prima */
/* === MODERN CARD COMPONENT === */
.card-modern {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 1.5rem;
transition: all 0.2s ease;
}
.card-modern:hover {
border-color: rgba(255, 255, 255, 0.12);
}
.card-header-modern {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-subtle);
}
.card-title {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-title i {
color: var(--primary);
}
/* === MODERN BUTTON === */
.btn-modern {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.625rem 1.25rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: var(--radius-md);
border: none;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-primary-modern {
background: var(--gradient-primary);
color: white;
}
.btn-primary-modern:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.35);
}
.btn-success-modern {
background: var(--gradient-success);
color: white;
}
.btn-danger-modern {
background: var(--gradient-danger);
color: white;
}
.btn-ghost {
background: transparent;
border: 1px solid var(--border-color);
color: var(--text-secondary);
}
.btn-ghost:hover {
background: var(--bg-hover);
border-color: rgba(255, 255, 255, 0.15);
color: var(--text-primary);
}
/* === MODERN INPUT === */
.input-modern {
width: 100%;
padding: 0.75rem 1rem;
font-size: 0.875rem;
color: var(--text-primary);
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
transition: all 0.15s ease;
}
.input-modern:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
}
.input-modern::placeholder {
color: var(--text-muted);
}
/* === BADGE === */
.badge-modern {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.625rem;
font-size: 0.75rem;
font-weight: 500;
border-radius: 9999px;
}
.badge-success {
background: rgba(34, 197, 94, 0.15);
color: #4ade80;
}
.badge-warning {
background: rgba(245, 158, 11, 0.15);
color: #fbbf24;
}
.badge-danger {
background: rgba(239, 68, 68, 0.15);
color: #f87171;
}
.badge-info {
background: rgba(59, 130, 246, 0.15);
color: #60a5fa;
}
/* === STAT CARD === */
.stat-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 1.25rem;
}
.stat-card-label {
font-size: 0.8125rem;
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.stat-card-value {
font-size: 1.75rem;
font-weight: 700;
color: var(--text-primary);
line-height: 1.2;
}
.stat-card-change {
font-size: 0.8125rem;
margin-top: 0.5rem;
}
.stat-card-change.positive {
color: var(--success);
}
.stat-card-change.negative {
color: var(--danger);
}
/* Sidebar Moderna - 260px */
.sidebar {
width: 250px;
width: 260px;
height: 100vh;
position: fixed;
left: 0;
top: 0;
background: linear-gradient(180deg, #1c2128 0%, #161b22 50%, #0d1117 100%);
background: linear-gradient(180deg, #1a1d23 0%, #13151a 100%);
border-right: 1px solid var(--border-color);
z-index: 1000;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5);
}
main {
margin-left: 250px;
margin-left: 260px;
flex: 1;
display: flex;
flex-direction: column;
@@ -355,6 +558,7 @@ main {
overflow: auto;
}
/* Splitter verticale tra griglia e log */
.splitter-vertical {
grid-column: 2;
@@ -363,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 */
@@ -395,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;
@@ -404,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 */
@@ -500,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 === */
@@ -669,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 {
@@ -1024,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);
}
@@ -1096,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 {
@@ -1128,7 +1397,7 @@ main {
}
.calc-item i {
font-size: 1.25rem;
font-size: 0.9rem;
color: var(--primary-color);
}
@@ -1137,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);
}
@@ -1152,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;
}
@@ -1195,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;
}
@@ -1216,7 +1485,7 @@ main {
}
.verdict-badge i {
font-size: 1.125rem;
font-size: 0.85rem;
}
/* === RESPONSIVE === */
@@ -1310,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 {
File diff suppressed because it is too large Load Diff
+15
View File
@@ -76,6 +76,7 @@
window.Blazor.addEventListener('enhancedload', initLogScroll);
}
// Esporta funzione per forzare scroll
window.forceLogScrollToBottom = function () {
logBoxes.forEach(logBox => {
@@ -83,4 +84,18 @@
scrollToBottom(logBox);
});
};
// Funzione chiamabile da Blazor per scroll specifico elemento
window.scrollToBottom = function (elementId) {
const element = document.getElementById(elementId);
if (element) {
// Controlla se siamo già in fondo o quasi (entro 100px)
const isNearBottom = element.scrollHeight - element.scrollTop - element.clientHeight < 100;
// Auto-scroll solo se siamo già in fondo (non interrompe lettura manuale)
if (isNearBottom || !userScrolling.get(element)) {
element.scrollTop = element.scrollHeight;
}
}
};
})();