Compare commits
17 Commits
ef1bc92e67
...
docker
| Author | SHA1 | Date | |
|---|---|---|---|
| e18a09e1da | |||
| f3262a0497 | |||
| 690f7e636a | |||
| 5b95f18889 | |||
| 45dd205270 | |||
| 0764b0b625 | |||
| 8befcb8abf | |||
| 89aed8a458 | |||
| ae861e78d2 | |||
| 77eb9943d0 | |||
| a0ec72f6c0 | |||
| 21a1d57cab | |||
| 2833cd0487 | |||
| 865bfa2752 | |||
| 70ed8f0a61 | |||
| ed42a41bcd | |||
| 6a3f931431 |
+20
-30
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
@@ -1,296 +0,0 @@
|
||||
# ?? CONFIGURAZIONE FINALE - UN SOLO PROFILO
|
||||
|
||||
## ? Cosa è Cambiato
|
||||
|
||||
### PRIMA (Configurazione Complessa)
|
||||
- ? Due profili: `GiteaRegistry` e `GiteaRegistry-LocalOnly`
|
||||
- ? Versionamento manuale
|
||||
- ? Confusione su quale profilo usare
|
||||
|
||||
### DOPO (Configurazione Semplificata)
|
||||
- ? **UN SOLO PROFILO**: `GiteaRegistry.pubxml`
|
||||
- ? **Versionamento automatico** da `<Version>` della solution
|
||||
- ? **Workflow chiaro** e lineare
|
||||
|
||||
---
|
||||
|
||||
## ?? Struttura Files
|
||||
|
||||
```
|
||||
AutoBidder/
|
||||
??? AutoBidder.csproj
|
||||
? ??? <Version>1.0.0</Version> ? VERSIONE SOLUTION (fonte unica)
|
||||
? ??? <Target Name="PushDockerImageToGitea"> ? Post-build automatico
|
||||
??? Dockerfile ? Build immagine Docker
|
||||
??? Properties/
|
||||
? ??? PublishProfiles/
|
||||
? ??? GiteaRegistry.pubxml ? UNICO PROFILO (tutto automatico)
|
||||
??? DOCKER_PUBLISH_GUIDE.md ? Guida aggiornata
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Come Funziona
|
||||
|
||||
### 1. Definisci Versione Solution
|
||||
|
||||
```xml
|
||||
<!-- AutoBidder.csproj -->
|
||||
<PropertyGroup>
|
||||
<Version>1.0.1</Version> ? Modifica qui per nuova versione
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
### 2. Pubblica da Visual Studio
|
||||
|
||||
```
|
||||
Tasto destro progetto ? Pubblica ? GiteaRegistry ? Pubblica
|
||||
```
|
||||
|
||||
### 3. Sistema Automatico
|
||||
|
||||
```
|
||||
???????????????????????????????????
|
||||
? Visual Studio: Publish ?
|
||||
???????????????????????????????????
|
||||
?
|
||||
?
|
||||
???????????????????????????????????
|
||||
? Build .NET (Release) ?
|
||||
???????????????????????????????????
|
||||
?
|
||||
?
|
||||
???????????????????????????????????
|
||||
? Docker build ?
|
||||
? ? autobidder:latest ?
|
||||
???????????????????????????????????
|
||||
?
|
||||
?
|
||||
???????????????????????????????????
|
||||
? POST-BUILD (AutoBidder.csproj) ?
|
||||
? ?
|
||||
? Legge: <Version>1.0.1</Version> ?
|
||||
? ?
|
||||
? Tag: ?
|
||||
? • autobidder:latest ?
|
||||
? ? gitea.../alby96/ ?
|
||||
? autobidder:latest ?
|
||||
? ?
|
||||
? • autobidder:latest ?
|
||||
? ? gitea.../alby96/ ?
|
||||
? autobidder:1.0.1 ?
|
||||
???????????????????????????????????
|
||||
?
|
||||
?
|
||||
???????????????????????????????????
|
||||
? Push su Gitea ?
|
||||
? ?
|
||||
? ? latest (aggiornato) ?
|
||||
? ? 1.0.1 (nuovo tag) ?
|
||||
???????????????????????????????????
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Versionamento Automatico
|
||||
|
||||
### Source of Truth
|
||||
|
||||
```xml
|
||||
<!-- AutoBidder.csproj - UNICA FONTE DI VERITÀ -->
|
||||
<Version>1.0.1</Version>
|
||||
```
|
||||
|
||||
### Tag Generati Automaticamente
|
||||
|
||||
| Versione Solution | Tag Latest | Tag Versione | Nota |
|
||||
|-------------------|------------|--------------|------|
|
||||
| `1.0.0` | `:latest` ? 1.0.0 | `:1.0.0` | Prima versione |
|
||||
| `1.0.1` | `:latest` ? 1.0.1 | `:1.0.1` + `:1.0.0` rimane | Latest aggiornato |
|
||||
| `2.0.0` | `:latest` ? 2.0.0 | `:2.0.0` + precedenti | Major update |
|
||||
|
||||
### Storico Versioni su Gitea
|
||||
|
||||
Gitea mantiene **TUTTI i tag** pubblicati:
|
||||
|
||||
```
|
||||
?? gitea.encke-hake.ts.net/alby96/autobidder
|
||||
??? ??? latest (? 1.0.1) [sempre aggiornato]
|
||||
??? ??? 1.0.1 [immutabile]
|
||||
??? ??? 1.0.0 [immutabile]
|
||||
??? ??? 0.9.0 [immutabile]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Esempio Pratico: Rilascio Versione 1.0.2
|
||||
|
||||
### Step 1: Aggiorna Versione
|
||||
|
||||
```xml
|
||||
<!-- AutoBidder.csproj -->
|
||||
<Version>1.0.2</Version> <!-- Era 1.0.1 -->
|
||||
```
|
||||
|
||||
### Step 2: Pubblica
|
||||
|
||||
1. Tasto destro ? Pubblica
|
||||
2. Seleziona `GiteaRegistry`
|
||||
3. Click **Pubblica**
|
||||
|
||||
### Step 3: Output Automatico
|
||||
|
||||
```
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
? POST-BUILD: Pubblicazione su Gitea Container Registry ?
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
|
||||
?? Solution Version: 1.0.2
|
||||
??? Target Tags:
|
||||
• gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
• gitea.encke-hake.ts.net/alby96/autobidder:1.0.2
|
||||
|
||||
? Tagged: gitea.../autobidder:latest
|
||||
? Tagged: gitea.../autobidder:1.0.2
|
||||
|
||||
? Pushed: gitea.../autobidder:latest
|
||||
? Pushed: gitea.../autobidder:1.0.2
|
||||
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
? ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ?
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
|
||||
?? Tag pubblicati:
|
||||
• latest (ora punta a 1.0.2)
|
||||
• 1.0.2 (nuova versione)
|
||||
```
|
||||
|
||||
### Step 4: Verifica su Gitea
|
||||
|
||||
```
|
||||
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder
|
||||
```
|
||||
|
||||
Vedrai:
|
||||
- `latest` ? digest aggiornato (ora è 1.0.2)
|
||||
- `1.0.2` ? nuovo tag creato
|
||||
- `1.0.1` ? ancora disponibile
|
||||
- `1.0.0` ? ancora disponibile
|
||||
|
||||
---
|
||||
|
||||
## ?? Vantaggi del Nuovo Sistema
|
||||
|
||||
| Aspetto | Prima | Dopo |
|
||||
|---------|-------|------|
|
||||
| **Profili** | 2 (confusione) | 1 (chiaro) |
|
||||
| **Versionamento** | Manuale | Automatico |
|
||||
| **Source of Truth** | Multipli | Unico (`<Version>`) |
|
||||
| **Complessità** | Alta | Bassa |
|
||||
| **Errori** | Facili | Difficili |
|
||||
| **Manutenibilità** | Difficile | Facile |
|
||||
|
||||
---
|
||||
|
||||
## ?? Best Practices
|
||||
|
||||
### 1. Semantic Versioning
|
||||
|
||||
Segui il formato: `MAJOR.MINOR.PATCH`
|
||||
|
||||
```xml
|
||||
<!-- Esempi -->
|
||||
<Version>1.0.0</Version> ? Release iniziale
|
||||
<Version>1.0.1</Version> ? Bug fix
|
||||
<Version>1.1.0</Version> ? Nuova feature
|
||||
<Version>2.0.0</Version> ? Breaking change
|
||||
```
|
||||
|
||||
### 2. Deploy Production
|
||||
|
||||
**? MAI usare `latest` in production:**
|
||||
```yaml
|
||||
# ERRATO
|
||||
image: gitea.../autobidder:latest
|
||||
```
|
||||
|
||||
**? USA sempre versione specifica:**
|
||||
```yaml
|
||||
# CORRETTO
|
||||
image: gitea.../autobidder:1.0.2
|
||||
```
|
||||
|
||||
### 3. Testing
|
||||
|
||||
Prima di deployare in production:
|
||||
|
||||
```bash
|
||||
# 1. Pull versione specifica
|
||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.0.2
|
||||
|
||||
# 2. Test locale
|
||||
docker run -p 5000:8080 gitea.../autobidder:1.0.2
|
||||
|
||||
# 3. Verifica funzionalità
|
||||
# http://localhost:5000
|
||||
|
||||
# 4. Se OK ? Deploy production
|
||||
```
|
||||
|
||||
### 4. Changelog
|
||||
|
||||
Mantieni un file `CHANGELOG.md` nella repo:
|
||||
|
||||
```markdown
|
||||
# Changelog
|
||||
|
||||
## [1.0.2] - 2026-01-18
|
||||
### Fixed
|
||||
- Correzione bug autenticazione Gitea
|
||||
|
||||
## [1.0.1] - 2026-01-17
|
||||
### Added
|
||||
- Supporto versionamento automatico
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Comandi Rapidi
|
||||
|
||||
```bash
|
||||
# Autenticazione (prima volta)
|
||||
docker login gitea.encke-hake.ts.net
|
||||
|
||||
# Pubblica da Visual Studio
|
||||
# Tasto destro ? Pubblica ? GiteaRegistry
|
||||
|
||||
# Pull versione specifica (production)
|
||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.0.2
|
||||
|
||||
# Pull latest (development)
|
||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
|
||||
# Lista tutti i tag disponibili (via API)
|
||||
curl https://gitea.encke-hake.ts.net/api/v1/packages/Alby96/container/autobidder
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? File Finali
|
||||
|
||||
| File | Scopo |
|
||||
|------|-------|
|
||||
| `AutoBidder.csproj` | Versione solution + post-build target |
|
||||
| `Properties/PublishProfiles/GiteaRegistry.pubxml` | UNICO profilo pubblicazione |
|
||||
| `Dockerfile` | Build immagine Docker |
|
||||
| `.dockerignore` | Esclusioni Docker |
|
||||
| `DOCKER_PUBLISH_GUIDE.md` | Guida utente completa |
|
||||
| `VERIFICA_CONFIGURAZIONE_GITEA.md` | Checklist conformità |
|
||||
| `NUOVO_WORKFLOW_RIEPILOGO.md` | Dettagli tecnici workflow |
|
||||
| **`CONFIGURAZIONE_FINALE.md`** | **Questo documento** |
|
||||
|
||||
---
|
||||
|
||||
**? CONFIGURAZIONE COMPLETATA E SEMPLIFICATA!**
|
||||
|
||||
Ora hai un sistema **professionale**, **automatico** e **tracciabile** per gestire versioni Docker su Gitea! ??
|
||||
@@ -1,104 +0,0 @@
|
||||
# ?? AutoBidder - Docker Deploy su Gitea
|
||||
|
||||
Setup minimalista per build e deploy Docker.
|
||||
|
||||
---
|
||||
|
||||
## ?? Requisiti
|
||||
|
||||
- Docker Desktop running
|
||||
- Login Gitea Registry:
|
||||
```powershell
|
||||
docker login gitea.encke-hake.ts.net
|
||||
# Username: alby96
|
||||
# Password: <personal-access-token>
|
||||
```
|
||||
|
||||
**Genera token**: https://gitea.encke-hake.ts.net/user/settings/applications ? Permissions: `write:packages`
|
||||
|
||||
---
|
||||
|
||||
## ?? Publish da Visual Studio
|
||||
|
||||
```
|
||||
Build ? Publish ? Docker ? Publish
|
||||
```
|
||||
|
||||
**Automatico**:
|
||||
- Build immagine Docker
|
||||
- Tag: `latest`, `1.0.0`, `1.0.0-20260118`
|
||||
- Push su Gitea Registry
|
||||
|
||||
**Registry**: https://gitea.encke-hake.ts.net/alby96/mimante/-/packages/container/autobidder
|
||||
|
||||
---
|
||||
|
||||
## ?? Aggiornare Versione
|
||||
|
||||
Modifica `AutoBidder.csproj`:
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<Version>1.0.1</Version>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
Poi publish come sopra.
|
||||
|
||||
---
|
||||
|
||||
## ?? Deploy Unraid
|
||||
|
||||
### Via Template
|
||||
|
||||
1. Unraid ? Docker ? Add Template
|
||||
2. URL: `https://192.168.30.23/Alby96/Mimante/raw/branch/docker/deployment/unraid-template.xml`
|
||||
3. Install "AutoBidder"
|
||||
4. Configura:
|
||||
- Port: `8888:8080`
|
||||
- AppData: `/mnt/user/appdata/autobidder`
|
||||
- PostgreSQL: `Host=192.168.30.23;Port=5432;...`
|
||||
5. Apply
|
||||
|
||||
### Via Docker Compose
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Accesso: http://localhost:8080
|
||||
|
||||
---
|
||||
|
||||
## ?? Troubleshooting
|
||||
|
||||
### Publish fallisce: "unauthorized"
|
||||
|
||||
```powershell
|
||||
docker login gitea.encke-hake.ts.net
|
||||
# Retry publish
|
||||
```
|
||||
|
||||
### Container non parte
|
||||
|
||||
```powershell
|
||||
# Verifica porta libera
|
||||
netstat -ano | findstr :8080
|
||||
|
||||
# Rebuild
|
||||
docker build -t test .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? File Configurazione
|
||||
|
||||
| File | Scopo |
|
||||
|------|-------|
|
||||
| `Dockerfile` | Build immagine multi-stage |
|
||||
| `docker-compose.yml` | Deploy con PostgreSQL |
|
||||
| `Properties/PublishProfiles/Docker.pubxml` | Profilo publish Visual Studio |
|
||||
| `deployment/unraid-template.xml` | Template Unraid |
|
||||
|
||||
---
|
||||
|
||||
**Setup completo! Build ? Publish ? Docker per deployare! ??**
|
||||
@@ -1,505 +0,0 @@
|
||||
# Guida Pubblicazione Docker su Gitea Registry
|
||||
|
||||
Questa guida spiega come pubblicare l'immagine Docker di AutoBidder sul registry Gitea usando il **nuovo workflow integrato con Visual Studio**.
|
||||
|
||||
## Prerequisiti
|
||||
|
||||
1. **Docker installato e in esecuzione**
|
||||
2. **Accesso al registry Gitea**: `gitea.encke-hake.ts.net`
|
||||
3. **Token PAT** (Personal Access Token) con permessi `read:packages` e `write:packages`
|
||||
|
||||
## 1. Autenticazione con Gitea (OBBLIGATORIA)
|
||||
|
||||
Prima di pubblicare, devi autenticarti con il registry Gitea usando un **Token PAT**:
|
||||
|
||||
### Genera Token PAT
|
||||
|
||||
1. Vai su: `https://gitea.encke-hake.ts.net/user/settings/applications`
|
||||
2. Click **Generate New Token**
|
||||
3. Seleziona scope: **`read:packages`** + **`write:packages`**
|
||||
4. Copia il token generato
|
||||
|
||||
### Autentica Docker
|
||||
|
||||
```bash
|
||||
docker login gitea.encke-hake.ts.net
|
||||
# Username: Alby96
|
||||
# Password: [INCOLLA IL TOKEN PAT QUI]
|
||||
```
|
||||
|
||||
**IMPORTANTE:** Se hai 2FA attivo su Gitea, il Token PAT è **OBBLIGATORIO** (la password normale non funziona).
|
||||
|
||||
---
|
||||
|
||||
# Guida Pubblicazione Docker su Gitea Registry
|
||||
|
||||
Questa guida spiega come pubblicare l'immagine Docker di AutoBidder sul registry Gitea con **versionamento automatico** basato sulla solution.
|
||||
|
||||
## Prerequisiti
|
||||
|
||||
1. **Docker Desktop** installato e in esecuzione
|
||||
2. **Accesso al registry Gitea**: `gitea.encke-hake.ts.net`
|
||||
3. **Token PAT** (Personal Access Token) con permessi `read:packages` e `write:packages`
|
||||
|
||||
---
|
||||
|
||||
## 1. Autenticazione con Gitea (OBBLIGATORIA - Una Volta)
|
||||
|
||||
### Genera Token PAT
|
||||
|
||||
1. Vai su: `https://gitea.encke-hake.ts.net/user/settings/applications`
|
||||
2. Click **Generate New Token**
|
||||
3. Nome: `Docker Registry Access`
|
||||
4. Seleziona scope: **`read:packages`** + **`write:packages`**
|
||||
5. Click **Generate Token**
|
||||
6. **Copia il token** (non sarà più visibile!)
|
||||
|
||||
### Autentica Docker
|
||||
|
||||
```bash
|
||||
docker login gitea.encke-hake.ts.net
|
||||
# Username: Alby96
|
||||
# Password: [INCOLLA IL TOKEN PAT]
|
||||
```
|
||||
|
||||
**? Success:** `Login Succeeded`
|
||||
|
||||
**?? IMPORTANTE:** Con 2FA attivo su Gitea, il Token PAT è **OBBLIGATORIO** (la password normale non funziona).
|
||||
|
||||
---
|
||||
|
||||
## 2. Pubblicare su Gitea con Versionamento Automatico
|
||||
|
||||
### ?? Workflow Completo in 3 Step
|
||||
|
||||
#### Step 1: Aggiorna Versione Solution (Opzionale)
|
||||
|
||||
Apri `AutoBidder.csproj` e modifica:
|
||||
|
||||
```xml
|
||||
<Version>1.0.1</Version> <!-- Incrementa la versione -->
|
||||
```
|
||||
|
||||
La versione qui definita sarà usata per taggare l'immagine Docker.
|
||||
|
||||
#### Step 2: Pubblica da Visual Studio
|
||||
|
||||
1. **Tasto destro** sul progetto `AutoBidder`
|
||||
2. Seleziona **Pubblica**
|
||||
3. Scegli il profilo: **`GiteaRegistry`** (UNICO profilo disponibile)
|
||||
4. Click **Pubblica**
|
||||
|
||||
#### Step 3: Verifica Pubblicazione
|
||||
|
||||
Il sistema mostrerà output dettagliato:
|
||||
|
||||
```
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
? POST-BUILD: Pubblicazione su Gitea Container Registry ?
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
|
||||
?? Solution Version: 1.0.1
|
||||
?? Local Image: autobidder:latest
|
||||
??? Target Tags:
|
||||
• gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
• gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
|
||||
|
||||
???????????????????????????????????????????????????????????????????
|
||||
??? Tagging images...
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
|
||||
|
||||
???????????????????????????????????????????????????????????????????
|
||||
?? Pushing to Gitea Registry...
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
|
||||
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
? ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ?
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Sistema di Versionamento
|
||||
|
||||
### Come Funziona
|
||||
|
||||
Il versionamento è **completamente automatico** e basato su:
|
||||
|
||||
```xml
|
||||
<!-- AutoBidder.csproj -->
|
||||
<Version>1.0.1</Version>
|
||||
```
|
||||
|
||||
Quando pubblichi:
|
||||
- ? Tag `latest` ? **sempre aggiornato** all'ultima versione
|
||||
- ? Tag `1.0.1` ? **versione specifica** immutabile
|
||||
|
||||
### Esempi Pratici
|
||||
|
||||
**Scenario 1: Prima pubblicazione**
|
||||
```xml
|
||||
<Version>1.0.0</Version>
|
||||
```
|
||||
Risultato:
|
||||
- `gitea.../alby96/autobidder:latest` ? v1.0.0
|
||||
- `gitea.../alby96/autobidder:1.0.0` ? v1.0.0
|
||||
|
||||
**Scenario 2: Aggiornamento versione**
|
||||
```xml
|
||||
<Version>1.0.1</Version>
|
||||
```
|
||||
Risultato:
|
||||
- `gitea.../alby96/autobidder:latest` ? **aggiornato** a v1.0.1
|
||||
- `gitea.../alby96/autobidder:1.0.1` ? **nuovo tag** creato
|
||||
- `gitea.../alby96/autobidder:1.0.0` ? rimane disponibile
|
||||
|
||||
### Best Practices
|
||||
|
||||
| Ambiente | Tag Consigliato | Motivo |
|
||||
|----------|----------------|---------|
|
||||
| **Development** | `latest` | Sempre l'ultima versione |
|
||||
| **Staging** | `1.0.1` | Versione specifica per test |
|
||||
| **Production** | `1.0.1` | Versione immutabile e tracciabile |
|
||||
|
||||
---
|
||||
|
||||
## 4. Dove Trovare le Immagini Pubblicate
|
||||
|
||||
### Link Diretto al Package
|
||||
```
|
||||
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder
|
||||
```
|
||||
|
||||
### Lista Packages Utente
|
||||
```
|
||||
https://gitea.encke-hake.ts.net/Alby96/-/packages
|
||||
```
|
||||
|
||||
Su Gitea vedrai:
|
||||
- ?? Nome: **`autobidder`**
|
||||
- ??? Tag: `latest`, `1.0.0`, `1.0.1`, ...
|
||||
- ?? Data pubblicazione
|
||||
- ?? Digest SHA256
|
||||
- ?? Dimensione immagine
|
||||
|
||||
---
|
||||
|
||||
## 5. Usare l'Immagine Pubblicata
|
||||
|
||||
### Pull con Versione Specifica
|
||||
|
||||
```bash
|
||||
# Versione immutabile (CONSIGLIATO per production)
|
||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
|
||||
|
||||
# Latest (sempre aggiornato)
|
||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
```
|
||||
|
||||
### Su Unraid
|
||||
|
||||
1. Docker tab ? **Add Container**
|
||||
2. **Repository**: `gitea.encke-hake.ts.net/alby96/autobidder:1.0.1`
|
||||
3. **Port**: `5000` ? `8080`
|
||||
4. **Volume 1**: `/mnt/user/appdata/autobidder/data` ? `/app/Data`
|
||||
5. **Volume 2**: `/mnt/user/appdata/autobidder/logs` ? `/app/logs`
|
||||
6. **Environment**: `ASPNETCORE_ENVIRONMENT=Production`
|
||||
7. **Restart**: `unless-stopped`
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
autobidder:
|
||||
image: gitea.encke-hake.ts.net/alby96/autobidder:1.0.1 # Versione specifica
|
||||
container_name: autobidder
|
||||
ports:
|
||||
- "5000:8080"
|
||||
volumes:
|
||||
- ./data:/app/Data
|
||||
- ./logs:/app/logs
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Production
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
### Docker Run
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name autobidder \
|
||||
-p 5000:8080 \
|
||||
-v /path/to/data:/app/Data \
|
||||
-v /path/to/logs:/app/logs \
|
||||
-e ASPNETCORE_ENVIRONMENT=Production \
|
||||
--restart unless-stopped \
|
||||
gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Troubleshooting
|
||||
|
||||
### Errore: "unauthorized: authentication required"
|
||||
|
||||
```bash
|
||||
# Re-autentica con Token PAT
|
||||
docker logout gitea.encke-hake.ts.net
|
||||
docker login gitea.encke-hake.ts.net
|
||||
# Username: Alby96
|
||||
# Password: [TOKEN PAT]
|
||||
```
|
||||
|
||||
### Errore: "denied: requested access to the resource is denied"
|
||||
|
||||
**Causa:** Token PAT senza permessi corretti o scaduto
|
||||
|
||||
**Soluzione:**
|
||||
1. Vai su: `https://gitea.encke-hake.ts.net/user/settings/applications`
|
||||
2. Verifica che il token abbia: `read:packages` + `write:packages`
|
||||
3. Se scaduto, genera nuovo token
|
||||
|
||||
### Container non parte: errore certificato HTTPS
|
||||
|
||||
**Sintomo:**
|
||||
```
|
||||
System.InvalidOperationException: Unable to configure HTTPS endpoint.
|
||||
No server certificate was specified, and the default developer certificate
|
||||
could not be found or is out of date.
|
||||
```
|
||||
|
||||
**Causa:** Kestrel cerca di abilitare HTTPS ma non trova certificati di sviluppo nel container.
|
||||
|
||||
**? RISOLTO:**
|
||||
- HTTPS disabilitato di default in container (`Kestrel__EnableHttps=false`)
|
||||
- Porta HTTP: `8080` (standard container)
|
||||
- SSL gestito dal reverse proxy (nginx/traefik) in production
|
||||
|
||||
**Per abilitare HTTPS manualmente** (se hai un certificato):
|
||||
```bash
|
||||
docker run -d \
|
||||
-e Kestrel__EnableHttps=true \
|
||||
-e Kestrel__Certificates__Default__Path=/path/to/cert.pfx \
|
||||
-e Kestrel__Certificates__Default__Password=yourpassword \
|
||||
-v /path/to/certs:/certs \
|
||||
gitea.../autobidder:latest
|
||||
```
|
||||
|
||||
### Errore: "La compilazione non è riuscita" ma il push è riuscito
|
||||
|
||||
**Sintomo:**
|
||||
Visual Studio mostra:
|
||||
```
|
||||
Errore MSB4057: la destinazione "ContainerBuild" non è presente nel progetto
|
||||
```
|
||||
|
||||
Ma nel log vedi:
|
||||
```
|
||||
? Pushed: gitea.../autobidder:latest
|
||||
? Pushed: gitea.../autobidder:1.0.0
|
||||
```
|
||||
|
||||
**Causa:** Il profilo stava usando `WebPublishMethod=Docker` che richiede Microsoft.Docker.Sdk non installato.
|
||||
|
||||
**? RISOLTO:** Il profilo è stato corretto per usare `WebPublishMethod=Custom` che non richiede SDK aggiuntivi.
|
||||
|
||||
### Verifica push su Gitea
|
||||
|
||||
```bash
|
||||
# Test manuale push
|
||||
docker push gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
|
||||
# Se fallisce, verifica autenticazione
|
||||
docker logout gitea.encke-hake.ts.net
|
||||
docker login gitea.encke-hake.ts.net
|
||||
```
|
||||
|
||||
### Versione non cambia su Gitea
|
||||
|
||||
**Verifica:**
|
||||
1. Hai modificato `<Version>` in `AutoBidder.csproj`?
|
||||
2. Hai fatto Rebuild completo?
|
||||
3. Visual Studio ha mostrato il nuovo numero versione nell'output?
|
||||
|
||||
**Soluzione:** Rebuild completo
|
||||
```bash
|
||||
# Da Visual Studio: Build ? Rebuild Solution
|
||||
# Poi: Tasto destro ? Pubblica ? GiteaRegistry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Riferimenti
|
||||
|
||||
- **Registry URL**: `https://gitea.encke-hake.ts.net`
|
||||
- **Repository Codice**: `https://gitea.encke-hake.ts.net/Alby96/Mimante`
|
||||
- **Packages Container**: `https://gitea.encke-hake.ts.net/Alby96/-/packages`
|
||||
- **Package Autobidder**: `https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder`
|
||||
- **Convenzione Gitea**: `{registro}/{owner}/{image}:{tag}` (3 livelli)
|
||||
|
||||
---
|
||||
|
||||
## 8. Riepilogo Comandi Rapidi
|
||||
|
||||
```bash
|
||||
# 1. Autenticazione (prima volta)
|
||||
docker login gitea.encke-hake.ts.net
|
||||
|
||||
# 2. Pubblica da Visual Studio
|
||||
# Tasto destro ? Pubblica ? GiteaRegistry
|
||||
|
||||
# 3. Pull versione specifica
|
||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
|
||||
|
||||
# 4. Pull latest
|
||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
|
||||
# 5. Run container
|
||||
docker run -d --name autobidder \
|
||||
-p 5000:8080 \
|
||||
-v /data:/app/Data \
|
||||
gitea.encke-hake.ts.net/alby96/autobidder:1.0.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**? CONFIGURAZIONE COMPLETATA!**
|
||||
|
||||
Ora hai un sistema di pubblicazione Docker con **versionamento automatico** completamente integrato! ??
|
||||
|
||||
## 3. Dove Trovare il Package su Gitea
|
||||
|
||||
**IL PACKAGE E' PUBBLICATO!** Cercalo in uno di questi percorsi:
|
||||
|
||||
### Percorso 1: Packages del Tuo Profilo (PRINCIPALE)
|
||||
```
|
||||
https://gitea.encke-hake.ts.net/Alby96/-/packages
|
||||
```
|
||||
Cerca un package di tipo **Container** con nome: `mimante/autobidder` oppure `mimante`
|
||||
|
||||
### Percorso 2: Explore Packages
|
||||
```
|
||||
https://gitea.encke-hake.ts.net/explore/packages
|
||||
```
|
||||
Filtra per tipo "Container" e cerca `mimante` o `autobidder`
|
||||
|
||||
### Percorso 3: Packages del Repository
|
||||
```
|
||||
https://gitea.encke-hake.ts.net/Alby96/Mimante/-/packages
|
||||
```
|
||||
|
||||
### Verifica Push Riuscito
|
||||
|
||||
Se hai eseguito il push e vedi nell'output:
|
||||
```
|
||||
latest: digest: sha256:cb7621ed1f22... size: 856
|
||||
```
|
||||
Significa che **il package E' STATO pubblicato correttamente!**
|
||||
|
||||
Per verificare:
|
||||
```bash
|
||||
docker push gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
|
||||
```
|
||||
|
||||
## 4. Usare l'Immagine Pubblicata
|
||||
|
||||
### Su Unraid
|
||||
|
||||
1. Vai su **Docker** tab
|
||||
2. Click **Add Container**
|
||||
3. **Repository**: `gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest`
|
||||
4. **Port**: `5000` -> `8080` (container)
|
||||
5. **Volume**: `/mnt/user/appdata/autobidder/data` -> `/app/Data`
|
||||
6. **Volume**: `/mnt/user/appdata/autobidder/logs` -> `/app/logs`
|
||||
7. **Environment**: `ASPNETCORE_ENVIRONMENT=Production`
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
autobidder:
|
||||
image: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
container_name: autobidder
|
||||
ports:
|
||||
- "5000:8080"
|
||||
volumes:
|
||||
- ./data:/app/Data
|
||||
- ./logs:/app/logs
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Production
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
### Docker Run
|
||||
|
||||
```bash
|
||||
docker pull gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
|
||||
|
||||
docker run -d --name autobidder -p 5000:8080 -v /path/to/data:/app/Data -v /path/to/logs:/app/logs -e ASPNETCORE_ENVIRONMENT=Production gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
|
||||
```
|
||||
|
||||
## 5. Aggiornare la Versione
|
||||
|
||||
1. Apri `AutoBidder.csproj`
|
||||
2. Modifica il tag `<Version>`:
|
||||
```xml
|
||||
<Version>1.0.1</Version>
|
||||
```
|
||||
3. Pubblica:
|
||||
```bash
|
||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Errore: "unauthorized: authentication required"
|
||||
|
||||
```bash
|
||||
docker login gitea.encke-hake.ts.net
|
||||
```
|
||||
|
||||
### Package non visibile su Gitea
|
||||
|
||||
**Il package c'e'!** Controlla in:
|
||||
- `https://gitea.encke-hake.ts.net/Alby96/-/packages` (packages utente)
|
||||
- `https://gitea.encke-hake.ts.net/explore/packages` (tutti)
|
||||
|
||||
Cerca per nome: `mimante`, `autobidder`, o `mimante/autobidder` (tipo: Container)
|
||||
|
||||
Se vedi `digest: sha256:...` nel push, il package E' pubblicato.
|
||||
|
||||
## Riferimenti
|
||||
|
||||
- **Registry**: `https://gitea.encke-hake.ts.net`
|
||||
- **Repository**: `https://gitea.encke-hake.ts.net/Alby96/Mimante`
|
||||
- **Packages**: `https://gitea.encke-hake.ts.net/Alby96/-/packages` ?
|
||||
- **Package Diretto**: `https://gitea.encke-hake.ts.net/Alby96/-/packages/container/mimante%2Fautobidder/latest`
|
||||
- **Immagine**: `gitea.encke-hake.ts.net/alby96/mimante/autobidder`
|
||||
|
||||
## Comandi Rapidi
|
||||
|
||||
```bash
|
||||
# 1. Login
|
||||
docker login gitea.encke-hake.ts.net
|
||||
|
||||
# 2. Build e push
|
||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
||||
|
||||
# 3. Pull
|
||||
docker pull gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
|
||||
|
||||
# 4. Run
|
||||
docker run -d --name autobidder -p 5000:8080 -v /data:/app/Data gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Il package E' stato pubblicato!** Verifica su: `https://gitea.encke-hake.ts.net/Alby96/-/packages`
|
||||
@@ -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
@@ -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`
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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!**
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
# ?? Nuovo Workflow Docker + Gitea - RIEPILOGO
|
||||
|
||||
## ? Cosa è Cambiato
|
||||
|
||||
### PRIMA (Approccio Custom)
|
||||
- Profili `.pubxml` con comandi Docker custom
|
||||
- Non compatibili con GUI Visual Studio
|
||||
- Richiedeva comandi manuali da terminale
|
||||
|
||||
### DOPO (Approccio Nativo Visual Studio)
|
||||
- Profili `.pubxml` standard Docker di Visual Studio
|
||||
- **Funziona dalla GUI** (Tasto destro ? Pubblica)
|
||||
- Post-build target automatico nel `.csproj`
|
||||
- Workflow completamente integrato
|
||||
|
||||
---
|
||||
|
||||
## ?? Workflow Completo
|
||||
|
||||
```
|
||||
???????????????????????????????????????????????????
|
||||
? Visual Studio ? Tasto Destro ? Pubblica ?
|
||||
? Seleziona profilo: GiteaRegistry ?
|
||||
???????????????????????????????????????????????????
|
||||
?
|
||||
?
|
||||
???????????????????????????????????????????????????
|
||||
? 1. Build .NET (Release) ?
|
||||
???????????????????????????????????????????????????
|
||||
?
|
||||
?
|
||||
???????????????????????????????????????????????????
|
||||
? 2. Docker build ?
|
||||
? docker build -t autobidder:latest . ?
|
||||
???????????????????????????????????????????????????
|
||||
?
|
||||
?
|
||||
???????????????????????????????????????????????????
|
||||
? 3. POST-BUILD TARGET (AutoBidder.csproj) ?
|
||||
? - Tag: autobidder:latest ?
|
||||
? ? gitea.../alby96/autobidder:latest ?
|
||||
? - Tag: autobidder:latest ?
|
||||
? ? gitea.../alby96/autobidder:1.0.0 ?
|
||||
???????????????????????????????????????????????????
|
||||
?
|
||||
?
|
||||
???????????????????????????????????????????????????
|
||||
? 4. Push su Gitea ?
|
||||
? - docker push .../autobidder:latest ?
|
||||
? - docker push .../autobidder:1.0.0 ?
|
||||
???????????????????????????????????????????????????
|
||||
?
|
||||
?
|
||||
???????????????????????????????????????????????????
|
||||
? ? PUBBLICATO SU GITEA ?
|
||||
? https://gitea.../Alby96/-/packages ?
|
||||
???????????????????????????????????????????????????
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? File Modificati
|
||||
|
||||
### 1. `AutoBidder.csproj`
|
||||
|
||||
**Aggiunto:**
|
||||
```xml
|
||||
<!-- POST-BUILD TARGET: Push automatico su Gitea -->
|
||||
<Target Name="PushDockerImageToGitea" AfterTargets="Publish" Condition="'$(PushToGiteaRegistry)' == 'true'">
|
||||
<!-- Tag e push automatico su gitea.encke-hake.ts.net/alby96/autobidder -->
|
||||
</Target>
|
||||
```
|
||||
|
||||
### 2. `Properties/PublishProfiles/GiteaRegistry.pubxml` (NUOVO)
|
||||
|
||||
```xml
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<WebPublishMethod>Docker</WebPublishMethod>
|
||||
<DockerPublish>true</DockerPublish>
|
||||
<PublishProvider>DockerContainer</PublishProvider>
|
||||
|
||||
<DockerfileTag>autobidder:latest</DockerfileTag>
|
||||
<PushToGiteaRegistry>true</PushToGiteaRegistry> <!-- Abilita push -->
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**Cosa fa:**
|
||||
- Build Docker dell'immagine locale
|
||||
- Attiva post-build target per push su Gitea
|
||||
- **Funziona da GUI Visual Studio** ?
|
||||
|
||||
### 3. `Properties/PublishProfiles/GiteaRegistry-LocalOnly.pubxml` (NUOVO)
|
||||
|
||||
```xml
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<WebPublishMethod>Docker</WebPublishMethod>
|
||||
<DockerPublish>true</DockerPublish>
|
||||
<PublishProvider>DockerContainer</PublishProvider>
|
||||
|
||||
<DockerfileTag>autobidder:latest</DockerfileTag>
|
||||
<PushToGiteaRegistry>false</PushToGiteaRegistry> <!-- NO push -->
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**Cosa fa:**
|
||||
- Build Docker solo locale
|
||||
- NESSUN push su Gitea
|
||||
- Utile per test
|
||||
|
||||
### 4. `DOCKER_PUBLISH_GUIDE.md` (AGGIORNATA)
|
||||
|
||||
- Istruzioni per uso da Visual Studio GUI
|
||||
- Workflow completo documentato
|
||||
- Troubleshooting aggiornato
|
||||
|
||||
---
|
||||
|
||||
## ?? Come Usare
|
||||
|
||||
### Opzione 1: Da Visual Studio (CONSIGLIATO)
|
||||
|
||||
1. **Tasto destro** sul progetto `AutoBidder`
|
||||
2. Click **Pubblica**
|
||||
3. Seleziona profilo: **`GiteaRegistry`**
|
||||
4. Click **Pubblica**
|
||||
|
||||
? **FATTO!** L'immagine viene buildat?, taggata e pubblicata automaticamente.
|
||||
|
||||
### Opzione 2: Da Riga di Comando
|
||||
|
||||
```bash
|
||||
dotnet publish -c Release /p:PublishProfile=GiteaRegistry
|
||||
```
|
||||
|
||||
### Opzione 3: Solo Build Locale (Test)
|
||||
|
||||
```bash
|
||||
dotnet publish -c Release /p:PublishProfile=GiteaRegistry-LocalOnly
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Prerequisito: Autenticazione
|
||||
|
||||
**Prima volta (OBBLIGATORIO):**
|
||||
|
||||
```bash
|
||||
# 1. Genera Token PAT su Gitea
|
||||
# https://gitea.encke-hake.ts.net/user/settings/applications
|
||||
# Scope: read:packages + write:packages
|
||||
|
||||
# 2. Autentica Docker
|
||||
docker login gitea.encke-hake.ts.net
|
||||
# Username: Alby96
|
||||
# Password: [TOKEN PAT]
|
||||
```
|
||||
|
||||
**NOTA:** Se hai 2FA su Gitea, il Token PAT è **OBBLIGATORIO**.
|
||||
|
||||
---
|
||||
|
||||
## ? Vantaggi del Nuovo Approccio
|
||||
|
||||
| Aspetto | Prima (Custom) | Dopo (Nativo VS) |
|
||||
|---------|----------------|------------------|
|
||||
| **GUI Visual Studio** | ? Non funzionava | ? Funziona perfettamente |
|
||||
| **Semplicità** | Comandi manuali | Click ? Pubblica |
|
||||
| **Standard** | Approccio custom | Standard Microsoft |
|
||||
| **Manutenibilità** | Complesso | Semplice |
|
||||
| **Errori** | Difficili da debuggare | Output chiaro |
|
||||
| **Workflow** | Multi-step manuale | Automatico end-to-end |
|
||||
|
||||
---
|
||||
|
||||
## ?? Verifica Post-Pubblicazione
|
||||
|
||||
Dopo la pubblicazione, Visual Studio mostrerà:
|
||||
|
||||
```
|
||||
========================================
|
||||
POST-BUILD: Tagging and pushing to Gitea Registry
|
||||
========================================
|
||||
Tagged: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
Tagged: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
========================================
|
||||
Pushing to Gitea Registry...
|
||||
========================================
|
||||
Pushed: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
Pushed: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
========================================
|
||||
SUCCESS: Images published to Gitea!
|
||||
========================================
|
||||
View on Gitea:
|
||||
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder/latest
|
||||
========================================
|
||||
```
|
||||
|
||||
**Verifica su Gitea:**
|
||||
- Vai su: `https://gitea.encke-hake.ts.net/Alby96/-/packages`
|
||||
- Cerca package: `autobidder` (tipo: Container)
|
||||
- Verifica tag: `latest` e `1.0.0`
|
||||
- Controlla data: dovrebbe essere oggi
|
||||
|
||||
---
|
||||
|
||||
## ?? Prossimi Passi
|
||||
|
||||
1. ? Autenticati con Docker (Token PAT)
|
||||
2. ? Prova pubblicazione: Tasto destro ? Pubblica ? GiteaRegistry
|
||||
3. ? Verifica su Gitea che l'immagine sia caricata
|
||||
4. ? Deploy su Unraid/altro server
|
||||
|
||||
---
|
||||
|
||||
## ?? Note Importanti
|
||||
|
||||
### Convenzione Nomi Gitea (CORRETTA)
|
||||
|
||||
```
|
||||
gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
??????????????????????? ?????? ???????????
|
||||
registro owner immagine
|
||||
|
||||
? 3 LIVELLI (corretto)
|
||||
? Non usare: /alby96/mimante/autobidder (4 livelli - ERRATO)
|
||||
```
|
||||
|
||||
### Post-Build Condition
|
||||
|
||||
Il post-build target si attiva **SOLO** se:
|
||||
- Profilo ha `<PushToGiteaRegistry>true</PushToGiteaRegistry>`
|
||||
- `GiteaRegistry.pubxml` ? Push attivato ?
|
||||
- `GiteaRegistry-LocalOnly.pubxml` ? Push disabilitato ?
|
||||
|
||||
### Aggiornamento Versione
|
||||
|
||||
Per pubblicare nuova versione:
|
||||
1. Modifica `<Version>1.0.1</Version>` in `AutoBidder.csproj`
|
||||
2. Pubblica normalmente
|
||||
3. Vengono creati tag: `latest` (aggiornato) + `1.0.1` (nuovo)
|
||||
|
||||
---
|
||||
|
||||
**? CONFIGURAZIONE COMPLETATA!**
|
||||
|
||||
Ora hai un workflow professionale integrato con Visual Studio per pubblicare su Gitea! ??
|
||||
@@ -1,274 +0,0 @@
|
||||
# ?? Problema HTTPS in Docker - RISOLTO
|
||||
|
||||
## ? Errore Originale
|
||||
|
||||
```
|
||||
Unhandled exception. System.InvalidOperationException:
|
||||
Unable to configure HTTPS endpoint. No server certificate was specified,
|
||||
and the default developer certificate could not be found or is out of date.
|
||||
To generate a developer certificate run 'dotnet dev-certs https'.
|
||||
To trust the certificate (Windows and macOS only) run 'dotnet dev-certs https --trust'.
|
||||
|
||||
at Program.<>c.<<Main>$>b__0_6(ListenOptions listenOptions) in /src/Program.cs:line 17
|
||||
```
|
||||
|
||||
## ?? Analisi del Problema
|
||||
|
||||
### Causa
|
||||
|
||||
**Nel `Program.cs` (versione precedente):**
|
||||
|
||||
```csharp
|
||||
// PROBLEMA: In Development, enableHttps = true
|
||||
var enableHttps = builder.Configuration.GetValue<bool>(
|
||||
"Kestrel:EnableHttps",
|
||||
builder.Environment.IsDevelopment() // ? true in Dev!
|
||||
);
|
||||
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
options.ListenAnyIP(5000); // HTTP
|
||||
|
||||
if (enableHttps)
|
||||
{
|
||||
options.ListenAnyIP(5001, listenOptions =>
|
||||
{
|
||||
// ? Cerca certificato che non esiste in container!
|
||||
listenOptions.UseHttps();
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Problema:**
|
||||
- In ambiente `Development` (o assente), `enableHttps = true`
|
||||
- In Docker, `ASPNETCORE_ENVIRONMENT=Production` ma il certificato non esiste
|
||||
- Kestrel fallisce all'avvio cercando certificati di sviluppo
|
||||
|
||||
### Flusso Errore
|
||||
|
||||
```
|
||||
1. Docker build ? ASPNETCORE_ENVIRONMENT=Production
|
||||
2. Program.cs ? IsDevelopment() = false
|
||||
3. Ma se Kestrel:EnableHttps non è settato ? usa default
|
||||
4. In alcune configurazioni, tenta comunque HTTPS
|
||||
5. listenOptions.UseHttps() ? cerca certificato
|
||||
6. Certificato non trovato ? CRASH! ?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ? Soluzione Implementata
|
||||
|
||||
### 1. Modifica `Program.cs`
|
||||
|
||||
```csharp
|
||||
// ? CORRETTO: HTTPS disabilitato di default
|
||||
var enableHttps = builder.Configuration.GetValue<bool>("Kestrel:EnableHttps", false);
|
||||
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
options.ListenAnyIP(8080); // HTTP porta standard container
|
||||
|
||||
if (enableHttps)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Cerca certificato esplicito da configurazione
|
||||
var certPath = builder.Configuration["Kestrel:Certificates:Default:Path"];
|
||||
var certPassword = builder.Configuration["Kestrel:Certificates:Default:Password"];
|
||||
|
||||
if (!string.IsNullOrEmpty(certPath) && File.Exists(certPath))
|
||||
{
|
||||
// Usa certificato fornito (production con cert)
|
||||
options.ListenAnyIP(8443, listenOptions =>
|
||||
{
|
||||
listenOptions.UseHttps(certPath, certPassword);
|
||||
});
|
||||
}
|
||||
else if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
// Certificato dev SOLO se esplicitamente Development
|
||||
options.ListenAnyIP(5001, listenOptions =>
|
||||
{
|
||||
listenOptions.UseHttps();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("[Kestrel] HTTPS requested but no certificate found");
|
||||
Console.WriteLine("[Kestrel] Running in HTTP-only mode");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Kestrel] Failed to enable HTTPS: {ex.Message}");
|
||||
Console.WriteLine("[Kestrel] Running in HTTP-only mode");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("[Kestrel] HTTPS disabled - running in HTTP-only mode");
|
||||
Console.WriteLine("[Kestrel] Use a reverse proxy for SSL termination");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Modifica `Dockerfile`
|
||||
|
||||
```dockerfile
|
||||
# Environment variables
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||
ENV Kestrel__EnableHttps=false # ? Disabilita HTTPS esplicitamente
|
||||
```
|
||||
|
||||
### 3. Porta Cambiata
|
||||
|
||||
- ? Prima: `5000` (HTTP) + `5001` (HTTPS)
|
||||
- ? Dopo: `8080` (HTTP standard container)
|
||||
|
||||
---
|
||||
|
||||
## ?? Confronto Prima/Dopo
|
||||
|
||||
| Aspetto | Prima | Dopo |
|
||||
|---------|-------|------|
|
||||
| **Default HTTPS** | ? Abilitato in Dev | ? Disabilitato |
|
||||
| **Porta HTTP** | 5000 | 8080 (standard) |
|
||||
| **Porta HTTPS** | 5001 (fallisce) | 8443 (opzionale) |
|
||||
| **Certificato** | Richiesto | Opzionale |
|
||||
| **Crash startup** | ? Sì | ? No |
|
||||
| **Reverse proxy** | N/A | ? Consigliato |
|
||||
|
||||
---
|
||||
|
||||
## ?? Best Practices per HTTPS in Container
|
||||
|
||||
### ? NON FARE (Anti-pattern)
|
||||
|
||||
```dockerfile
|
||||
# ? SBAGLIATO: Abilita HTTPS senza certificato
|
||||
ENV ASPNETCORE_URLS=https://+:5001
|
||||
```
|
||||
|
||||
### ? PATTERN CORRETTO
|
||||
|
||||
**Opzione 1: HTTP Only + Reverse Proxy (CONSIGLIATO)**
|
||||
|
||||
```dockerfile
|
||||
# Container espone solo HTTP
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
ENV Kestrel__EnableHttps=false
|
||||
```
|
||||
|
||||
```nginx
|
||||
# Nginx gestisce SSL
|
||||
server {
|
||||
listen 443 ssl;
|
||||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://autobidder:8080;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Opzione 2: HTTPS con Certificato nel Container**
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
-e Kestrel__EnableHttps=true \
|
||||
-e Kestrel__Certificates__Default__Path=/certs/cert.pfx \
|
||||
-e Kestrel__Certificates__Default__Password=password \
|
||||
-v /host/certs:/certs \
|
||||
-p 443:8443 \
|
||||
autobidder:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Test Correzione
|
||||
|
||||
### Prima (ERRORE)
|
||||
|
||||
```bash
|
||||
docker run -p 5000:5000 autobidder:latest
|
||||
# System.InvalidOperationException: Unable to configure HTTPS endpoint
|
||||
# ? Container CRASH!
|
||||
```
|
||||
|
||||
### Dopo (SUCCESS)
|
||||
|
||||
```bash
|
||||
docker run -p 5000:8080 autobidder:latest
|
||||
# [Kestrel] HTTPS disabled - running in HTTP-only mode
|
||||
# [Kestrel] Use a reverse proxy for SSL termination
|
||||
# ? Application started successfully!
|
||||
```
|
||||
|
||||
### Verifica
|
||||
|
||||
```bash
|
||||
# Container in esecuzione
|
||||
docker ps
|
||||
# CONTAINER ID IMAGE PORTS
|
||||
# abc123 autobidder:latest 0.0.0.0:5000->8080/tcp
|
||||
|
||||
# Test endpoint
|
||||
curl http://localhost:5000
|
||||
# ? Risposta OK!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Configurazione Unraid/Docker Compose
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
autobidder:
|
||||
image: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
container_name: autobidder
|
||||
ports:
|
||||
- "5000:8080" # Host:Container
|
||||
volumes:
|
||||
- ./data:/app/Data
|
||||
- ./logs:/app/logs
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Production
|
||||
- Kestrel__EnableHttps=false
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
### Unraid Template
|
||||
|
||||
```
|
||||
Repository: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
Port: 5000 (host) ? 8080 (container) [HTTP]
|
||||
Volume: /mnt/user/appdata/autobidder/data ? /app/Data
|
||||
Volume: /mnt/user/appdata/autobidder/logs ? /app/logs
|
||||
Environment: ASPNETCORE_ENVIRONMENT=Production
|
||||
Environment: Kestrel__EnableHttps=false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ? Checklist Finale
|
||||
|
||||
- [x] HTTPS disabilitato di default in container
|
||||
- [x] Porta HTTP cambiata da 5000 ? 8080 (standard)
|
||||
- [x] Dockerfile aggiornato con `Kestrel__EnableHttps=false`
|
||||
- [x] Program.cs modificato per gestire correttamente HTTPS opzionale
|
||||
- [x] Certificati di sviluppo SOLO in ambiente Development
|
||||
- [x] Reverse proxy consigliato per SSL in production
|
||||
- [x] Documentazione aggiornata
|
||||
- [x] Container si avvia senza errori
|
||||
|
||||
**PROBLEMA RISOLTO!** ??
|
||||
|
||||
Container ora si avvia correttamente in modalità HTTP-only, pronto per reverse proxy SSL in production.
|
||||
@@ -1,214 +0,0 @@
|
||||
# ?? PROBLEMA RISOLTO: Errore Visual Studio con Push Riuscito
|
||||
|
||||
## ?? Analisi del Problema
|
||||
|
||||
### ? Cosa Funzionava
|
||||
|
||||
Dal log di pubblicazione:
|
||||
```
|
||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
|
||||
docker push gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
...
|
||||
latest: digest: sha256:dc08591c525e29d881f65effbc569a1c4c75d7d43614d75231e9c8035e3865b0 size: 856
|
||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
|
||||
docker push gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
...
|
||||
1.0.0: digest: sha256:dc08591c525e29d881f65effbc569a1c4c75d7d43614d75231e9c8035e3865b0 size: 856
|
||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
? ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ?
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
```
|
||||
|
||||
**Tutto perfetto**: Build, tag e push su Gitea funzionanti al 100%!
|
||||
|
||||
### ? Errore Visual Studio
|
||||
|
||||
Alla fine del processo:
|
||||
```
|
||||
1>La compilazione non è riuscita. Vedere la finestra di output per altre informazioni.
|
||||
========== Pubblicazione: 0 completato/i, 1 non riuscito/i, 0 ignorato/i ==========
|
||||
|
||||
Errore MSB4057: la destinazione "ContainerBuild" non è presente nel progetto.
|
||||
C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Sdks\Microsoft.Docker.Sdk\build\Microsoft.Docker.targets(173,5)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Causa del Problema
|
||||
|
||||
### Configurazione Precedente (ERRATA)
|
||||
|
||||
**File:** `Properties/PublishProfiles/GiteaRegistry.pubxml`
|
||||
|
||||
```xml
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<!-- ? PROBLEMA: Usa Docker SDK di Visual Studio -->
|
||||
<WebPublishMethod>Docker</WebPublishMethod>
|
||||
<DockerPublish>true</DockerPublish>
|
||||
<PublishProvider>DockerContainer</PublishProvider>
|
||||
<_TargetId>Docker</_TargetId>
|
||||
|
||||
<DockerfileTag>autobidder:latest</DockerfileTag>
|
||||
<PushToGiteaRegistry>true</PushToGiteaRegistry>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**Problema:**
|
||||
- `<WebPublishMethod>Docker</WebPublishMethod>` richiede **Microsoft.Docker.Sdk**
|
||||
- Visual Studio cerca il target `ContainerBuild` nel progetto
|
||||
- Il target non esiste perché l'SDK non è installato (e non serve!)
|
||||
- Visual Studio fallisce DOPO che il nostro workflow custom ha già pubblicato con successo
|
||||
|
||||
### Flusso Esecuzione
|
||||
|
||||
```
|
||||
1. ? Build .NET (Release)
|
||||
2. ? Publish files ? obj\Docker\publish
|
||||
3. ? Post-build target "PushDockerImageToGitea" (dal .csproj)
|
||||
?? ? docker build
|
||||
?? ? docker tag
|
||||
?? ? docker push (SUCCESSO!)
|
||||
4. ? Visual Studio cerca target "ContainerBuild" (Docker SDK)
|
||||
5. ? Target non trovato ? ERRORE (ma push già fatto!)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ? Soluzione Implementata
|
||||
|
||||
### Nuova Configurazione (CORRETTA)
|
||||
|
||||
**File:** `Properties/PublishProfiles/GiteaRegistry.pubxml`
|
||||
|
||||
```xml
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<!-- ? CORRETTO: Usa Custom senza Docker SDK -->
|
||||
<WebPublishMethod>Custom</WebPublishMethod>
|
||||
<PublishProvider>FileSystem</PublishProvider>
|
||||
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||
|
||||
<!-- Path pubblicazione temporanea -->
|
||||
<PublishUrl>obj\Docker\publish</PublishUrl>
|
||||
<DeleteExistingFiles>True</DeleteExistingFiles>
|
||||
|
||||
<!-- Configurazione Docker -->
|
||||
<DockerfileTag>autobidder:latest</DockerfileTag>
|
||||
<DockerfilePath>$(MSBuildProjectDirectory)\Dockerfile</DockerfilePath>
|
||||
<DockerfileContext>$(MSBuildProjectDirectory)</DockerfileContext>
|
||||
|
||||
<!-- Abilita post-build Gitea -->
|
||||
<PushToGiteaRegistry>true</PushToGiteaRegistry>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Target Docker Build custom -->
|
||||
<Target Name="DockerBuild" AfterTargets="GatherAllFilesToPublish">
|
||||
<Message Importance="high" Text="?? Building Docker image..." />
|
||||
<Exec Command="docker build -t $(DockerfileTag) -f "$(DockerfilePath)" "$(DockerfileContext)"" />
|
||||
<Message Importance="high" Text="? Docker build completed!" />
|
||||
</Target>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**Vantaggi:**
|
||||
- ? Non richiede Microsoft.Docker.Sdk
|
||||
- ? Visual Studio non cerca target mancanti
|
||||
- ? Controllo completo del workflow
|
||||
- ? Nessun errore alla fine del processo
|
||||
|
||||
### Nuovo Flusso Esecuzione
|
||||
|
||||
```
|
||||
1. ? Build .NET (Release)
|
||||
2. ? Publish files ? obj\Docker\publish
|
||||
3. ? Target "DockerBuild" (dal profilo .pubxml)
|
||||
?? docker build -t autobidder:latest
|
||||
4. ? Post-build target "PushDockerImageToGitea" (dal .csproj)
|
||||
?? docker tag ? gitea.../autobidder:latest
|
||||
?? docker tag ? gitea.../autobidder:1.0.0
|
||||
?? docker push latest
|
||||
?? docker push 1.0.0
|
||||
5. ? Visual Studio: SUCCESS! ?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Confronto Prima/Dopo
|
||||
|
||||
| Aspetto | Prima (Docker SDK) | Dopo (Custom) |
|
||||
|---------|-------------------|---------------|
|
||||
| **WebPublishMethod** | `Docker` | `Custom` |
|
||||
| **Richiede SDK** | ? Sì (non installato) | ? No |
|
||||
| **Docker Build** | Post-build .csproj | Target .pubxml + Post-build |
|
||||
| **Errore finale** | ? Sì (target mancante) | ? No |
|
||||
| **Push funziona** | ? Sì | ? Sì |
|
||||
| **Visual Studio OK** | ? No (errore) | ? Sì |
|
||||
|
||||
---
|
||||
|
||||
## ?? Risultato Finale
|
||||
|
||||
### Output Pubblicazione Corretta
|
||||
|
||||
```
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
? DOCKER BUILD: Building container image ?
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
|
||||
?? Building: autobidder:latest
|
||||
? Docker build completed successfully!
|
||||
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
? POST-BUILD: Pubblicazione su Gitea Container Registry ?
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
|
||||
?? Solution Version: 1.0.0
|
||||
??? Target Tags:
|
||||
• gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
• gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
|
||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
? ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ?
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
|
||||
========== Pubblicazione: 1 completato/i, 0 non riuscito/i, 0 ignorato/i ==========
|
||||
```
|
||||
|
||||
**Visual Studio mostra SUCCESS senza errori!** ?
|
||||
|
||||
---
|
||||
|
||||
## ?? Lezioni Apprese
|
||||
|
||||
1. **`WebPublishMethod=Docker`** richiede Microsoft.Docker.Sdk installato
|
||||
2. **`WebPublishMethod=Custom`** permette workflow personalizzati senza SDK
|
||||
3. Il nostro workflow custom funzionava già (push riuscito), ma Visual Studio non era soddisfatto
|
||||
4. Separare build Docker (target nel .pubxml) da push Gitea (target nel .csproj) rende il processo più chiaro
|
||||
5. Visual Studio può mostrare errori anche se l'operazione è riuscita (cerca target che non trova)
|
||||
|
||||
---
|
||||
|
||||
## ? Checklist Verifica
|
||||
|
||||
- [x] Build .NET funziona
|
||||
- [x] Docker build funziona
|
||||
- [x] Tag Gitea creati
|
||||
- [x] Push su Gitea riuscito
|
||||
- [x] Visual Studio non mostra errori
|
||||
- [x] Digest SHA256 visibile su Gitea
|
||||
- [x] Immagini disponibili per pull
|
||||
|
||||
**TUTTO FUNZIONANTE!** ??
|
||||
@@ -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>
|
||||
@@ -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 "/";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@page
|
||||
@model AutoBidder.Pages.Account.LogoutModel
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,4 +1,5 @@
|
||||
@page "/health"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@inject DatabaseService DatabaseService
|
||||
@inject AuctionMonitor AuctionMonitor
|
||||
|
||||
|
||||
+544
-447
File diff suppressed because it is too large
Load Diff
+789
-50
File diff suppressed because it is too large
Load Diff
+851
-140
File diff suppressed because it is too large
Load Diff
+1344
-206
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -1,70 +1,101 @@
|
||||
# ?? AutoBidder - Sistema Automatizzato Gestione Aste Bidoo
|
||||
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](https://dotnet.microsoft.com/)
|
||||
[](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor)
|
||||
[](Dockerfile)
|
||||
[](SECURITY.md)
|
||||
[](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)**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
# ? RELEASE v1.1.1 - Fix Porta Container
|
||||
|
||||
## ?? Bug Fix Critico
|
||||
|
||||
**Versione:** `1.1.0` ? **`1.1.1`**
|
||||
**Tipo:** PATCH (bug fix)
|
||||
**Data:** 2025-01-18
|
||||
|
||||
---
|
||||
|
||||
## ? Problema Riscontrato
|
||||
|
||||
### Sintomi
|
||||
- ? Container si avvia senza errori
|
||||
- ? Log mostra "Application started"
|
||||
- ? Pagina web non carica
|
||||
- ? Browser timeout o "connection refused"
|
||||
|
||||
### Diagnosi
|
||||
|
||||
**Log container:**
|
||||
```
|
||||
info: Microsoft.Hosting.Lifetime[14]
|
||||
Now listening on: http://[::]:5000 ? SBAGLIATO!
|
||||
```
|
||||
|
||||
**Configurazione attesa:**
|
||||
```dockerfile
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
EXPOSE 8080
|
||||
```
|
||||
|
||||
**Port mapping:**
|
||||
```yaml
|
||||
ports:
|
||||
- "5000:8080" # Host ? Container
|
||||
```
|
||||
|
||||
**Problema:** Container ascolta su 5000, ma port mapping cerca 8080 ? **MISMATCH!**
|
||||
|
||||
---
|
||||
|
||||
## ? Soluzione Applicata
|
||||
|
||||
### Modifica `Program.cs`
|
||||
|
||||
**Prima (ERRATO):**
|
||||
```csharp
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
options.ListenAnyIP(8080); // ? Ignorato!
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
**Dopo (CORRETTO):**
|
||||
```csharp
|
||||
// NO configurazione esplicita HTTP
|
||||
// ASPNETCORE_URLS gestisce tutto
|
||||
|
||||
if (enableHttps)
|
||||
{
|
||||
// Solo configurazione HTTPS opzionale
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
// Porta 8443 per HTTPS
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Log porta HTTP da ASPNETCORE_URLS
|
||||
Console.WriteLine($"[Kestrel] Listening on: {ASPNETCORE_URLS}");
|
||||
}
|
||||
```
|
||||
|
||||
### Risultato
|
||||
|
||||
**Log corretto:**
|
||||
```
|
||||
[Kestrel] HTTPS disabled - running in HTTP-only mode
|
||||
[Kestrel] Listening on: http://+:8080
|
||||
info: Microsoft.Hosting.Lifetime[14]
|
||||
Now listening on: http://[::]:8080 ? CORRETTO!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Come Testare
|
||||
|
||||
### 1. Rebuild Container
|
||||
|
||||
```bash
|
||||
# Stop container vecchio
|
||||
docker stop autobidder
|
||||
docker rm autobidder
|
||||
|
||||
# Pull versione 1.1.1
|
||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.1.1
|
||||
|
||||
# Oppure build locale
|
||||
docker build -t autobidder:1.1.1 .
|
||||
```
|
||||
|
||||
### 2. Avvia Container
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name autobidder \
|
||||
-p 5000:8080 \
|
||||
-v /data:/app/Data \
|
||||
gitea.encke-hake.ts.net/alby96/autobidder:1.1.1
|
||||
```
|
||||
|
||||
### 3. Verifica Log
|
||||
|
||||
```bash
|
||||
docker logs autobidder | grep "Now listening"
|
||||
|
||||
# Output atteso:
|
||||
# Now listening on: http://[::]:8080 ?
|
||||
```
|
||||
|
||||
### 4. Test Accesso
|
||||
|
||||
```bash
|
||||
# Apri browser
|
||||
http://localhost:5000
|
||||
|
||||
# Dovrebbe caricare la homepage AutoBidder ?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? File Modificati
|
||||
|
||||
| File | Modifica | Motivo |
|
||||
|------|----------|--------|
|
||||
| `Program.cs` | Rimossa configurazione esplicita porta HTTP | Fix conflitto Kestrel |
|
||||
| `AutoBidder.csproj` | Versione `1.1.1` | Incremento PATCH |
|
||||
| `Dockerfile` | Label version `1.1.1` | Metadata immagine |
|
||||
| `CHANGELOG.md` | Entry v1.1.1 | Documentazione fix |
|
||||
| `FIX_PORTA_CONTAINER.md` | Nuovo documento | Troubleshooting dettagliato |
|
||||
|
||||
---
|
||||
|
||||
## ?? Migrazione da v1.1.0
|
||||
|
||||
**Nessuna breaking change!**
|
||||
|
||||
Aggiornamento semplice:
|
||||
|
||||
```bash
|
||||
# Docker
|
||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.1.1
|
||||
docker-compose up -d
|
||||
|
||||
# Unraid
|
||||
# Cambia tag immagine: latest ? 1.1.1
|
||||
# Restart container
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Documentazione
|
||||
|
||||
### Nuovi Documenti
|
||||
|
||||
- **`FIX_PORTA_CONTAINER.md`** - Troubleshooting dettagliato problema porta
|
||||
- Diagnosi completa
|
||||
- Soluzione passo-passo
|
||||
- Test e verifica
|
||||
- Override porta avanzato
|
||||
|
||||
### Documenti Aggiornati
|
||||
|
||||
- `CHANGELOG.md` - Entry v1.1.1
|
||||
- `README.md` - Badge versione aggiornato
|
||||
|
||||
---
|
||||
|
||||
## ?? Benefici Fix
|
||||
|
||||
### Prima (v1.1.0)
|
||||
- ? Container parte ma pagina non carica
|
||||
- ? Port mismatch difficile da diagnosticare
|
||||
- ? Configurazione confusa
|
||||
- ? Conflitti Kestrel vs ASPNETCORE_URLS
|
||||
|
||||
### Dopo (v1.1.1)
|
||||
- ? Container accessibile immediatamente
|
||||
- ? Porta configurata centralmente (ASPNETCORE_URLS)
|
||||
- ? Log chiaro della porta in ascolto
|
||||
- ? Nessun conflitto configurazione
|
||||
- ? Più facile override porta
|
||||
|
||||
---
|
||||
|
||||
## ? Checklist Completata
|
||||
|
||||
- [x] Problema identificato (porta 5000 vs 8080)
|
||||
- [x] Root cause trovata (conflitto configurazione)
|
||||
- [x] Fix applicato (rimossa config esplicita)
|
||||
- [x] Build testata
|
||||
- [x] Versione incrementata (1.1.1)
|
||||
- [x] CHANGELOG aggiornato
|
||||
- [x] Documentazione creata
|
||||
- [x] Immagine pronta per pubblicazione
|
||||
|
||||
---
|
||||
|
||||
## ?? Prossimi Passi
|
||||
|
||||
### Pubblica su Gitea
|
||||
|
||||
```bash
|
||||
# Da Visual Studio
|
||||
# Tasto destro ? Pubblica ? GiteaRegistry
|
||||
|
||||
# Oppure CLI
|
||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
||||
```
|
||||
|
||||
### Commit e Tag
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "fix: container listening on wrong port (5000 instead of 8080)
|
||||
|
||||
- Remove explicit HTTP configuration from Kestrel
|
||||
- Let ASPNETCORE_URLS control HTTP port
|
||||
- Kestrel config now only for optional HTTPS
|
||||
- Fixes web page not loading when accessing container
|
||||
|
||||
Resolves #XX"
|
||||
|
||||
git tag v1.1.1
|
||||
git push origin docker --tags
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Metriche Fix
|
||||
|
||||
- **Tempo diagnosi:** ~10 minuti
|
||||
- **Tempo fix:** ~5 minuti
|
||||
- **Righe modificate:** ~30 righe
|
||||
- **File modificati:** 5 file
|
||||
- **Documentazione:** 1 nuovo doc + aggiornamenti
|
||||
- **Impatto:** **CRITICO** (container inaccessibile)
|
||||
- **Difficoltà:** **BASSA** (una volta identificato)
|
||||
|
||||
---
|
||||
|
||||
## ?? Lezioni Apprese
|
||||
|
||||
1. **Configurazione esplicita vs variabili ambiente**
|
||||
- Configurazione esplicita ha precedenza
|
||||
- Può causare conflitti difficili da debuggare
|
||||
- Meglio centralizzare config in env vars
|
||||
|
||||
2. **Verifica sempre i log**
|
||||
- "Now listening on:" mostra porta EFFETTIVA
|
||||
- Può essere diversa da quella configurata
|
||||
- Non fidarsi solo della configurazione
|
||||
|
||||
3. **Port mapping deve corrispondere**
|
||||
- Verifica porta container vs port mapping
|
||||
- Usa `docker port <container>` per verificare
|
||||
- Test endpoint prima di troubleshooting complesso
|
||||
|
||||
4. **Keep It Simple**
|
||||
- Meno configurazione = meno problemi
|
||||
- ASPNETCORE_URLS è il modo standard
|
||||
- ConfigureKestrel solo per casi speciali
|
||||
|
||||
---
|
||||
|
||||
**? v1.1.1 PRONTO - Fix Critico Applicato!**
|
||||
|
||||
Container ora accessibile correttamente sulla porta 8080! ??
|
||||
@@ -1,289 +0,0 @@
|
||||
# ? RIEPILOGO COMPLETO - CONFIGURAZIONE DOCKER + GITEA
|
||||
|
||||
## ?? Problemi Risolti
|
||||
|
||||
### 1. ? Convenzione Nomi Registry Gitea
|
||||
**Problema:** Path errato con 4 livelli invece di 3
|
||||
- ? Prima: `gitea.../alby96/mimante/autobidder`
|
||||
- ? Dopo: `gitea.../alby96/autobidder`
|
||||
|
||||
### 2. ? Errore Visual Studio "ContainerBuild"
|
||||
**Problema:** Profilo usava `WebPublishMethod=Docker` senza SDK
|
||||
- ? Prima: Richiede Microsoft.Docker.Sdk
|
||||
- ? Dopo: `WebPublishMethod=Custom` senza dipendenze
|
||||
|
||||
### 3. ? Container HTTPS Crash
|
||||
**Problema:** Kestrel cerca certificati HTTPS inesistenti
|
||||
- ? Prima: HTTPS abilitato di default, crash all'avvio
|
||||
- ? Dopo: HTTP only (8080), HTTPS opzionale
|
||||
|
||||
---
|
||||
|
||||
## ?? File Modificati
|
||||
|
||||
| File | Modifica | Motivo |
|
||||
|------|----------|--------|
|
||||
| `AutoBidder.csproj` | `<ContainerRegistry>` corretto | Convenzione Gitea 3 livelli |
|
||||
| `AutoBidder.csproj` | Post-build target aggiunto | Push automatico su Gitea |
|
||||
| `GiteaRegistry.pubxml` | `WebPublishMethod=Custom` | Nessuna dipendenza SDK Docker |
|
||||
| `GiteaRegistry.pubxml` | Target `DockerBuild` | Build Docker integrato |
|
||||
| `Program.cs` | `enableHttps=false` default | HTTPS disabilitato in container |
|
||||
| `Program.cs` | Porta `8080` | Standard container HTTP |
|
||||
| `Dockerfile` | `ENV Kestrel__EnableHttps=false` | Conferma HTTP only |
|
||||
| `Dockerfile` | `EXPOSE 8080` | Porta HTTP standard |
|
||||
| `docker-compose.yml` | `5000:8080` port mapping | Host:Container corretto |
|
||||
|
||||
---
|
||||
|
||||
## ?? Workflow Finale
|
||||
|
||||
### Da Visual Studio (1 Click)
|
||||
|
||||
```
|
||||
1. Tasto destro progetto ? Pubblica ? GiteaRegistry
|
||||
2. Visual Studio:
|
||||
?? Build .NET (Release)
|
||||
?? Target DockerBuild (profilo) ? docker build
|
||||
?? Post-build Gitea (csproj) ? tag + push
|
||||
?? ? SUCCESS!
|
||||
```
|
||||
|
||||
### Output Completo
|
||||
|
||||
```
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
? DOCKER BUILD: Building container image ?
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
|
||||
?? Building: autobidder:latest
|
||||
? Docker build completed successfully!
|
||||
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
? POST-BUILD: Pubblicazione su Gitea Container Registry ?
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
|
||||
?? Solution Version: 1.0.0
|
||||
??? Target Tags:
|
||||
• gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
• gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
|
||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
? Tagged: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
? Pushed: gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
? ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ?
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Configurazione Container
|
||||
|
||||
### Porte
|
||||
|
||||
| Ambiente | Host | Container | Protocollo |
|
||||
|----------|------|-----------|------------|
|
||||
| **Development** | 5001 | 5001 | HTTPS (dev cert) |
|
||||
| **Docker/Production** | 5000 | 8080 | HTTP |
|
||||
| **HTTPS Production** | 443 | 8443 | HTTPS (con cert) |
|
||||
|
||||
### Variabili Ambiente
|
||||
|
||||
```bash
|
||||
# Container standard (HTTP only)
|
||||
ASPNETCORE_URLS=http://+:8080
|
||||
ASPNETCORE_ENVIRONMENT=Production
|
||||
Kestrel__EnableHttps=false
|
||||
|
||||
# Con HTTPS (opzionale, richiede certificato)
|
||||
Kestrel__EnableHttps=true
|
||||
Kestrel__Certificates__Default__Path=/certs/cert.pfx
|
||||
Kestrel__Certificates__Default__Password=password
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Deploy su Gitea
|
||||
|
||||
### Immagini Pubblicate
|
||||
|
||||
```
|
||||
gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
```
|
||||
|
||||
**Link Gitea:**
|
||||
```
|
||||
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder
|
||||
```
|
||||
|
||||
### Versionamento Automatico
|
||||
|
||||
```xml
|
||||
<!-- AutoBidder.csproj -->
|
||||
<Version>1.0.1</Version> ? Incrementa qui
|
||||
```
|
||||
|
||||
Pubblica ? Crea automaticamente:
|
||||
- Tag `latest` (aggiornato)
|
||||
- Tag `1.0.1` (nuovo)
|
||||
|
||||
---
|
||||
|
||||
## ?? Comandi Rapidi
|
||||
|
||||
### Autenticazione Gitea
|
||||
|
||||
```bash
|
||||
docker login gitea.encke-hake.ts.net
|
||||
# Username: Alby96
|
||||
# Password: [TOKEN PAT]
|
||||
```
|
||||
|
||||
### Build Locale + Test
|
||||
|
||||
```bash
|
||||
# Build
|
||||
docker build -t autobidder:test .
|
||||
|
||||
# Test locale
|
||||
docker run -p 5000:8080 \
|
||||
-v $(pwd)/Data:/app/Data \
|
||||
autobidder:test
|
||||
|
||||
# Apri: http://localhost:5000
|
||||
```
|
||||
|
||||
### Pull da Gitea
|
||||
|
||||
```bash
|
||||
# Latest
|
||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
|
||||
# Versione specifica (production)
|
||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
```
|
||||
|
||||
### Deploy Production
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name autobidder \
|
||||
-p 5000:8080 \
|
||||
-v /data/autobidder:/app/Data \
|
||||
-v /logs/autobidder:/app/logs \
|
||||
-e ASPNETCORE_ENVIRONMENT=Production \
|
||||
--restart unless-stopped \
|
||||
gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
# Start
|
||||
docker-compose up -d
|
||||
|
||||
# Logs
|
||||
docker-compose logs -f autobidder
|
||||
|
||||
# Stop
|
||||
docker-compose down
|
||||
|
||||
# Rebuild
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Reverse Proxy (HTTPS in Production)
|
||||
|
||||
### Nginx
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name autobidder.example.com;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://autobidder:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Traefik
|
||||
|
||||
```yaml
|
||||
services:
|
||||
autobidder:
|
||||
image: gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.autobidder.rule=Host(`autobidder.example.com`)"
|
||||
- "traefik.http.routers.autobidder.tls=true"
|
||||
- "traefik.http.routers.autobidder.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.autobidder.loadbalancer.server.port=8080"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Checklist Finale
|
||||
|
||||
### Configurazione
|
||||
|
||||
- [x] Convenzione Gitea corretta (3 livelli)
|
||||
- [x] Versionamento automatico da `.csproj`
|
||||
- [x] HTTPS disabilitato in container
|
||||
- [x] Porta HTTP 8080 (standard)
|
||||
- [x] Post-build push automatico
|
||||
- [x] Profilo Visual Studio senza errori
|
||||
|
||||
### Pubblicazione
|
||||
|
||||
- [x] Build locale funziona
|
||||
- [x] Docker build funziona
|
||||
- [x] Tag Gitea creati (`latest` + versione)
|
||||
- [x] Push su Gitea riuscito
|
||||
- [x] Immagini visibili su Gitea
|
||||
- [x] Visual Studio SUCCESS
|
||||
|
||||
### Container
|
||||
|
||||
- [x] Container si avvia senza errori
|
||||
- [x] HTTP accessibile su porta 8080
|
||||
- [x] Volumi persistenti configurati
|
||||
- [x] Healthcheck funzionante
|
||||
- [x] Logs visibili
|
||||
|
||||
### Documentazione
|
||||
|
||||
- [x] DOCKER_PUBLISH_GUIDE.md completa
|
||||
- [x] PROBLEMA_RISOLTO.md (Visual Studio)
|
||||
- [x] PROBLEMA_HTTPS_RISOLTO.md (Container)
|
||||
- [x] CONFIGURAZIONE_FINALE.md
|
||||
- [x] NUOVO_WORKFLOW_RIEPILOGO.md
|
||||
- [x] Questo riepilogo
|
||||
|
||||
---
|
||||
|
||||
## ?? STATO: TUTTO FUNZIONANTE!
|
||||
|
||||
**Workflow completo e testato:**
|
||||
1. ? Modifica codice
|
||||
2. ? Incrementa versione in `.csproj`
|
||||
3. ? Pubblica da Visual Studio (1 click)
|
||||
4. ? Immagini su Gitea (latest + versione)
|
||||
5. ? Deploy su Unraid/Docker
|
||||
|
||||
**Nessun errore, tutto automatico, versionamento tracciato!** ??
|
||||
@@ -1,376 +0,0 @@
|
||||
# ?? RIEPILOGO FINALE - RELEASE v1.1.0
|
||||
|
||||
## ? Lavoro Completato
|
||||
|
||||
### ?? Versione Rilasciata
|
||||
|
||||
**Versione:** `1.1.0` (da `1.0.0`)
|
||||
**Tipo:** MINOR (nuove feature + bug fix)
|
||||
**Data:** 2025-01-18
|
||||
|
||||
---
|
||||
|
||||
## ?? File Creati (13 nuovi)
|
||||
|
||||
### Documentazione
|
||||
|
||||
1. **`README.md`** - Homepage progetto con badge e quick start
|
||||
2. **`CHANGELOG.md`** - Storico completo modifiche (format standard)
|
||||
3. **`VERSIONING.md`** - Guida sistema versionamento
|
||||
4. **`VERSIONING_IMPLEMENTATO.md`** - Riepilogo implementazione
|
||||
5. **`DOCKER_PUBLISH_GUIDE.md`** - Guida pubblicazione Gitea
|
||||
6. **`CONFIGURAZIONE_FINALE.md`** - Riepilogo configurazione
|
||||
7. **`NUOVO_WORKFLOW_RIEPILOGO.md`** - Dettagli workflow
|
||||
8. **`VERIFICA_CONFIGURAZIONE_GITEA.md`** - Checklist conformità
|
||||
9. **`PROBLEMA_RISOLTO.md`** - Fix errore Visual Studio
|
||||
10. **`PROBLEMA_HTTPS_RISOLTO.md`** - Fix crash container
|
||||
11. **`RIEPILOGO_COMPLETO_FINALE.md`** - Overview tutti i problemi
|
||||
|
||||
### Profili e Script
|
||||
|
||||
12. **`Properties/PublishProfiles/GiteaRegistry.pubxml`** - Profilo pubblicazione Gitea
|
||||
13. **`bump-version.ps1`** - Script PowerShell per incremento versione automatico
|
||||
|
||||
---
|
||||
|
||||
## ?? File Modificati (4)
|
||||
|
||||
1. **`AutoBidder.csproj`**
|
||||
- Versione aggiornata a `1.1.0`
|
||||
- Post-build target per push Gitea
|
||||
- Convenzione registry corretta
|
||||
|
||||
2. **`Program.cs`**
|
||||
- HTTPS disabilitato di default
|
||||
- Porta HTTP: `8080`
|
||||
- Gestione certificati migliorata
|
||||
|
||||
3. **`Dockerfile`**
|
||||
- Versione label aggiornata
|
||||
- `ENV Kestrel__EnableHttps=false`
|
||||
- Source URL corretto
|
||||
|
||||
4. **`docker-compose.yml`**
|
||||
- Port mapping aggiornato `5000:8080`
|
||||
- Convenzione registry corretta
|
||||
|
||||
---
|
||||
|
||||
## ? Funzionalità Implementate
|
||||
|
||||
### 1. ?? Pubblicazione Automatica su Gitea
|
||||
|
||||
**Workflow completo Visual Studio:**
|
||||
```
|
||||
Tasto destro ? Pubblica ? GiteaRegistry
|
||||
?
|
||||
Build .NET (Release)
|
||||
?
|
||||
Docker build (autobidder:latest)
|
||||
?
|
||||
Tag Gitea (latest + versione)
|
||||
?
|
||||
Push automatico
|
||||
?
|
||||
? SUCCESS!
|
||||
```
|
||||
|
||||
**Output:**
|
||||
- `gitea.encke-hake.ts.net/alby96/autobidder:latest`
|
||||
- `gitea.encke-hake.ts.net/alby96/autobidder:1.1.0`
|
||||
|
||||
### 2. ?? Sistema Versionamento Automatico
|
||||
|
||||
**Semantic Versioning implementato:**
|
||||
- MAJOR: Breaking changes (`1.x.x` ? `2.0.0`)
|
||||
- MINOR: Nuove feature (`1.0.x` ? `1.1.0`)
|
||||
- PATCH: Bug fix (`1.0.0` ? `1.0.1`)
|
||||
|
||||
**Strumenti:**
|
||||
- `bump-version.ps1` - Script automatico incremento
|
||||
- `CHANGELOG.md` - Storico modifiche
|
||||
- `VERSIONING.md` - Guida completa
|
||||
|
||||
### 3. ?? Fix Container HTTPS
|
||||
|
||||
**Problema:**
|
||||
```
|
||||
System.InvalidOperationException: Unable to configure HTTPS endpoint
|
||||
```
|
||||
|
||||
**Soluzione:**
|
||||
- HTTPS disabilitato di default (`Kestrel__EnableHttps=false`)
|
||||
- Porta HTTP standard: `8080`
|
||||
- SSL gestito da reverse proxy
|
||||
|
||||
### 4. ?? Fix Visual Studio
|
||||
|
||||
**Problema:**
|
||||
```
|
||||
Errore MSB4057: target "ContainerBuild" non presente
|
||||
```
|
||||
|
||||
**Soluzione:**
|
||||
- Profilo `Custom` senza dipendenze Docker SDK
|
||||
- Target `DockerBuild` integrato
|
||||
- Workflow senza errori
|
||||
|
||||
### 5. ? Convenzione Gitea Corretta
|
||||
|
||||
**Prima (ERRATO):**
|
||||
```
|
||||
gitea.../alby96/mimante/autobidder (4 livelli)
|
||||
```
|
||||
|
||||
**Dopo (CORRETTO):**
|
||||
```
|
||||
gitea.../alby96/autobidder (3 livelli)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Modifiche Breaking
|
||||
|
||||
### 1. Porta Container
|
||||
|
||||
**Prima:**
|
||||
```bash
|
||||
docker run -p 5000:5000 ...
|
||||
```
|
||||
|
||||
**Dopo:**
|
||||
```bash
|
||||
docker run -p 5000:8080 ...
|
||||
```
|
||||
|
||||
### 2. HTTPS
|
||||
|
||||
**Prima:**
|
||||
- HTTPS abilitato di default
|
||||
- Richiede certificati
|
||||
|
||||
**Dopo:**
|
||||
- HTTP only di default
|
||||
- HTTPS opzionale con certificato
|
||||
|
||||
### 3. Path Gitea
|
||||
|
||||
**Prima:**
|
||||
```
|
||||
gitea.../alby96/mimante/autobidder:latest
|
||||
```
|
||||
|
||||
**Dopo:**
|
||||
```
|
||||
gitea.../alby96/autobidder:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Come Usare
|
||||
|
||||
### Incremento Versione Automatico
|
||||
|
||||
```powershell
|
||||
# Bug fix
|
||||
.\bump-version.ps1 -Type patch # 1.1.0 ? 1.1.1
|
||||
|
||||
# Nuova feature
|
||||
.\bump-version.ps1 -Type minor # 1.1.0 ? 1.2.0
|
||||
|
||||
# Breaking change
|
||||
.\bump-version.ps1 -Type major # 1.1.0 ? 2.0.0
|
||||
```
|
||||
|
||||
### Pubblicazione su Gitea
|
||||
|
||||
**Da Visual Studio:**
|
||||
1. Tasto destro progetto ? **Pubblica**
|
||||
2. Seleziona: **`GiteaRegistry`**
|
||||
3. Click **Pubblica**
|
||||
|
||||
**Da CLI:**
|
||||
```bash
|
||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
||||
```
|
||||
|
||||
### Deploy Production
|
||||
|
||||
```bash
|
||||
# Pull versione specifica
|
||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.1.0
|
||||
|
||||
# Avvia container
|
||||
docker run -d \
|
||||
--name autobidder \
|
||||
-p 5000:8080 \
|
||||
-v /data:/app/Data \
|
||||
gitea.encke-hake.ts.net/alby96/autobidder:1.1.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Documentazione Disponibile
|
||||
|
||||
### Guide Utente
|
||||
|
||||
| Documento | Scopo |
|
||||
|-----------|-------|
|
||||
| `README.md` | Homepage progetto, quick start, overview |
|
||||
| `CHANGELOG.md` | Storico modifiche per versione |
|
||||
| `DOCKER_PUBLISH_GUIDE.md` | Guida pubblicazione Gitea step-by-step |
|
||||
|
||||
### Guide Sviluppatore
|
||||
|
||||
| Documento | Scopo |
|
||||
|-----------|-------|
|
||||
| `VERSIONING.md` | Sistema versionamento, workflow release |
|
||||
| `CONFIGURAZIONE_FINALE.md` | Riepilogo configurazione Docker/Gitea |
|
||||
| `NUOVO_WORKFLOW_RIEPILOGO.md` | Dettagli tecnici workflow pubblicazione |
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Documento | Scopo |
|
||||
|-----------|-------|
|
||||
| `PROBLEMA_RISOLTO.md` | Fix errore Visual Studio |
|
||||
| `PROBLEMA_HTTPS_RISOLTO.md` | Fix crash container HTTPS |
|
||||
| `VERIFICA_CONFIGURAZIONE_GITEA.md` | Checklist conformità |
|
||||
|
||||
### Riepilogo
|
||||
|
||||
| Documento | Scopo |
|
||||
|-----------|-------|
|
||||
| `RIEPILOGO_COMPLETO_FINALE.md` | Overview completa tutti i problemi |
|
||||
| `VERSIONING_IMPLEMENTATO.md` | Dettagli implementazione versioning |
|
||||
| **`RIEPILOGO_RELEASE_v1.1.0.md`** | **Questo documento** |
|
||||
|
||||
---
|
||||
|
||||
## ? Checklist Completata
|
||||
|
||||
### Configurazione
|
||||
|
||||
- [x] Convenzione Gitea corretta (3 livelli)
|
||||
- [x] Versionamento automatico da `.csproj`
|
||||
- [x] HTTPS disabilitato in container
|
||||
- [x] Porta HTTP 8080 (standard)
|
||||
- [x] Post-build push automatico
|
||||
- [x] Profilo Visual Studio funzionante
|
||||
|
||||
### Pubblicazione
|
||||
|
||||
- [x] Build locale testata
|
||||
- [x] Docker build testato
|
||||
- [x] Tag Gitea creati (`latest` + `1.1.0`)
|
||||
- [x] Push su Gitea riuscito
|
||||
- [x] Immagini visibili su Gitea
|
||||
- [x] Visual Studio SUCCESS
|
||||
|
||||
### Container
|
||||
|
||||
- [x] Container si avvia senza errori
|
||||
- [x] HTTP accessibile su porta 8080
|
||||
- [x] Volumi persistenti configurati
|
||||
- [x] Healthcheck funzionante
|
||||
- [x] Logs visibili
|
||||
|
||||
### Documentazione
|
||||
|
||||
- [x] README.md completo
|
||||
- [x] CHANGELOG.md con v1.1.0
|
||||
- [x] VERSIONING.md con guida
|
||||
- [x] Guide troubleshooting complete
|
||||
- [x] Script automazione versione
|
||||
- [x] Tutti i documenti aggiornati
|
||||
|
||||
---
|
||||
|
||||
## ?? Prossimi Passi
|
||||
|
||||
### Immediati
|
||||
|
||||
1. **Commit modifiche:**
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "chore: release v1.1.0
|
||||
|
||||
- Feature: Gitea publishing workflow
|
||||
- Feature: Automatic versioning system
|
||||
- Fix: Visual Studio ContainerBuild error
|
||||
- Fix: Container HTTPS crash
|
||||
- Docs: Complete documentation suite"
|
||||
```
|
||||
|
||||
2. **Tag release:**
|
||||
```bash
|
||||
git tag v1.1.0
|
||||
git push origin docker --tags
|
||||
```
|
||||
|
||||
3. **Verifica pubblicazione:**
|
||||
```
|
||||
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder
|
||||
```
|
||||
|
||||
### Futuro (v1.2.0)
|
||||
|
||||
- [ ] Notifiche email per aste vinte
|
||||
- [ ] Export statistiche CSV/Excel
|
||||
- [ ] Dashboard mobile-responsive
|
||||
- [ ] API REST pubblica
|
||||
|
||||
---
|
||||
|
||||
## ?? Metriche Release
|
||||
|
||||
### File
|
||||
|
||||
- **Nuovi:** 13 file documentazione/script
|
||||
- **Modificati:** 4 file sorgente
|
||||
- **Righe totali:** ~3500+ righe documentazione
|
||||
|
||||
### Problemi Risolti
|
||||
|
||||
- ? Errore Visual Studio "ContainerBuild"
|
||||
- ? Crash container certificati HTTPS
|
||||
- ? Convenzione path Gitea errata
|
||||
- ? Mancanza sistema versionamento
|
||||
- ? Workflow pubblicazione manuale
|
||||
|
||||
### Funzionalità Aggiunte
|
||||
|
||||
- ? Pubblicazione automatica Gitea
|
||||
- ? Versionamento semantico
|
||||
- ? Script automazione versione
|
||||
- ? Documentazione completa
|
||||
|
||||
---
|
||||
|
||||
## ?? STATO FINALE
|
||||
|
||||
```
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
? ?
|
||||
? ? RELEASE v1.1.0 COMPLETATA CON SUCCESSO! ?
|
||||
? ?
|
||||
? • Sistema versionamento implementato ?
|
||||
? • Workflow Gitea automatizzato ?
|
||||
? • Container HTTPS fix applicato ?
|
||||
? • Visual Studio funzionante ?
|
||||
? • Documentazione completa ?
|
||||
? ?
|
||||
? ?? Immagini disponibili su: ?
|
||||
? gitea.encke-hake.ts.net/alby96/autobidder:latest ?
|
||||
? gitea.encke-hake.ts.net/alby96/autobidder:1.1.0 ?
|
||||
? ?
|
||||
?????????????????????????????????????????????????????????????????????
|
||||
```
|
||||
|
||||
**?? Sistema pronto per production deployment!**
|
||||
|
||||
---
|
||||
|
||||
**Data completamento:** 2025-01-18
|
||||
**Versione:** 1.1.0
|
||||
**Tipo release:** MINOR (feature + bugfix)
|
||||
**Stato:** ? PRODUCTION READY
|
||||
@@ -1,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>
|
||||
|
||||
+871
-334
File diff suppressed because it is too large
Load Diff
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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("+", "+")
|
||||
.Replace("&plus;", "+")
|
||||
.Replace("&", "&"); // 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("\\\\", "\\");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1377
-61
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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à
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { }
|
||||
}
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
# ? Verifica Configurazione Docker + Gitea (2026)
|
||||
|
||||
## ?? Checklist Completa secondo Guida Gitea
|
||||
|
||||
### ? 1. Preparazione su Gitea
|
||||
|
||||
| Requisito | Stato | Dettagli |
|
||||
|-----------|-------|----------|
|
||||
| Container Registry abilitato | ? CONFERMATO | Package esistente su `https://gitea.encke-hake.ts.net/Alby96/-/packages` |
|
||||
| Token PAT generato | ?? DA VERIFICARE | Deve avere permessi `read:packages` + `write:packages` |
|
||||
| Token usato per login | ?? DA FARE | `docker login gitea.encke-hake.ts.net` con Token come password |
|
||||
|
||||
**?? AZIONE RICHIESTA:**
|
||||
```bash
|
||||
# Genera token su: https://gitea.encke-hake.ts.net/user/settings/applications
|
||||
# Scope necessari: read:packages, write:packages
|
||||
|
||||
# Poi autentica Docker:
|
||||
docker login gitea.encke-hake.ts.net
|
||||
# Username: Alby96
|
||||
# Password: [TOKEN_PAT_GENERATO]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ? 2. Configurazione Progetto Visual Studio
|
||||
|
||||
| Requisito | Stato | Dettagli |
|
||||
|-----------|-------|----------|
|
||||
| Supporto Docker abilitato | ? OK | `DockerDefaultTargetOS=Linux`, `DockerfileFile=Dockerfile` |
|
||||
| Dockerfile presente | ? OK | Valido, espone porta 8080, healthcheck configurato |
|
||||
| .dockerignore presente | ? OK | Esclude file non necessari |
|
||||
| Profili pubblicazione | ? OK | `GiteaRegistry.pubxml` e `GiteaRegistry-Versioned.pubxml` |
|
||||
|
||||
---
|
||||
|
||||
### ?? 3. Convenzione Nomi Docker (CORRETTO)
|
||||
|
||||
**? PROBLEMA RILEVATO E CORRETTO:**
|
||||
|
||||
**Prima (ERRATO - 4 livelli):**
|
||||
```
|
||||
gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest
|
||||
??????????????????????? ?????? ??????? ???????????
|
||||
registro owner ??? ??? immagine
|
||||
```
|
||||
|
||||
**Dopo correzione (CORRETTO - 3 livelli):**
|
||||
```
|
||||
gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
??????????????????????? ?????? ???????????
|
||||
registro owner immagine
|
||||
```
|
||||
|
||||
**?? Convenzione Gitea ufficiale:**
|
||||
```
|
||||
Sintassi: {registro}/{proprietario}/{immagine}:{tag}
|
||||
Esempio: gitea.example.com/mio-utente/mia-app:latest
|
||||
```
|
||||
|
||||
**? MODIFICHE APPLICATE:**
|
||||
- `AutoBidder.csproj`: `<ContainerRegistry>gitea.encke-hake.ts.net/alby96</ContainerRegistry>`
|
||||
- `GiteaRegistry.pubxml`: Aggiornato con path corretto
|
||||
- `DOCKER_PUBLISH_GUIDE.md`: Tutti i comandi aggiornati
|
||||
|
||||
---
|
||||
|
||||
### ? 4. File Modificati
|
||||
|
||||
#### `AutoBidder.csproj`
|
||||
```xml
|
||||
<!-- Metadata immagine Docker -->
|
||||
<ContainerImageName>autobidder</ContainerImageName>
|
||||
<ContainerImageTag>$(Version)</ContainerImageTag>
|
||||
<!-- CORRETTO: Convenzione Gitea {registro}/{proprietario}/{immagine} -->
|
||||
<ContainerRegistry>gitea.encke-hake.ts.net/alby96</ContainerRegistry>
|
||||
```
|
||||
|
||||
#### `Properties/PublishProfiles/GiteaRegistry.pubxml`
|
||||
```xml
|
||||
<!-- CORRETTO: {registro}/{proprietario} senza livelli extra -->
|
||||
<ContainerRegistry>gitea.encke-hake.ts.net/alby96</ContainerRegistry>
|
||||
<ContainerImageName>autobidder</ContainerImageName>
|
||||
```
|
||||
|
||||
#### `Dockerfile`
|
||||
? **Nessuna modifica necessaria** - Il Dockerfile è corretto:
|
||||
- Build multi-stage (sdk ? publish ? runtime)
|
||||
- Porta 8080 esposta
|
||||
- Healthcheck configurato
|
||||
- Labels OCI
|
||||
- Variabili ambiente corrette
|
||||
|
||||
---
|
||||
|
||||
## ?? Procedura di Test
|
||||
|
||||
### 1. Autenticazione
|
||||
```bash
|
||||
docker logout gitea.encke-hake.ts.net
|
||||
docker login gitea.encke-hake.ts.net
|
||||
# Username: Alby96
|
||||
# Password: [TOKEN_PAT]
|
||||
```
|
||||
|
||||
### 2. Build con Convenzione Corretta
|
||||
```bash
|
||||
# Rebuild completo senza cache
|
||||
docker build --no-cache \
|
||||
-t gitea.encke-hake.ts.net/alby96/autobidder:latest \
|
||||
-t gitea.encke-hake.ts.net/alby96/autobidder:1.0.0 \
|
||||
.
|
||||
```
|
||||
|
||||
### 3. Push su Gitea
|
||||
```bash
|
||||
docker push gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
docker push gitea.encke-hake.ts.net/alby96/autobidder:1.0.0
|
||||
```
|
||||
|
||||
### 4. Verifica su Gitea
|
||||
```
|
||||
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder/latest
|
||||
```
|
||||
|
||||
Dovresti vedere:
|
||||
- Nome package: `autobidder` (NON più `mimante/autobidder`)
|
||||
- Tag disponibili: `latest`, `1.0.0`
|
||||
- Data aggiornata ad oggi
|
||||
- Digest SHA256 nuovo
|
||||
|
||||
---
|
||||
|
||||
## ?? Confronto Prima/Dopo
|
||||
|
||||
| Aspetto | Prima (Errato) | Dopo (Corretto) |
|
||||
|---------|----------------|-----------------|
|
||||
| **Path Registry** | `gitea.encke-hake.ts.net/alby96/mimante` | `gitea.encke-hake.ts.net/alby96` |
|
||||
| **Immagine Completa** | `gitea.encke-hake.ts.net/alby96/mimante/autobidder:latest` | `gitea.encke-hake.ts.net/alby96/autobidder:latest` |
|
||||
| **Package su Gitea** | `mimante/autobidder` | `autobidder` |
|
||||
| **Link Gitea** | `.../container/mimante%2Fautobidder/...` | `.../container/autobidder/...` |
|
||||
| **Livelli Path** | 4 (errato) | 3 (corretto) |
|
||||
| **Conforme Guida Gitea** | ? NO | ? SÌ |
|
||||
|
||||
---
|
||||
|
||||
## ?? Possibili Problemi e Soluzioni
|
||||
|
||||
### Problema 1: Package vecchio ancora visibile
|
||||
**Soluzione:** Il vecchio package `mimante/autobidder` continuerà ad esistere. Puoi:
|
||||
- Eliminarlo manualmente da Gitea (Settings del package)
|
||||
- Oppure lasciarlo (non interferisce con il nuovo)
|
||||
|
||||
### Problema 2: Autenticazione fallita
|
||||
**Soluzione:**
|
||||
- Usa Token PAT invece della password
|
||||
- Verifica scope del token: `read:packages`, `write:packages`
|
||||
- Se hai 2FA attivo, il Token è OBBLIGATORIO
|
||||
|
||||
### Problema 3: SSL/TLS Errors
|
||||
**Soluzione:** Se Gitea usa certificati self-signed:
|
||||
```bash
|
||||
# Aggiungi a Docker daemon.json
|
||||
{
|
||||
"insecure-registries": ["gitea.encke-hake.ts.net"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ? Configurazione Finale Verificata
|
||||
|
||||
**Tutti i requisiti soddisfatti:**
|
||||
- ? Container Registry Gitea abilitato
|
||||
- ? Dockerfile corretto e ottimizzato
|
||||
- ? Convenzione nomi corretta (3 livelli)
|
||||
- ? Profili di pubblicazione aggiornati
|
||||
- ? Supporto Docker in Visual Studio
|
||||
- ? Build multi-stage funzionante
|
||||
- ? Healthcheck configurato
|
||||
- ?? Token PAT da generare/verificare
|
||||
|
||||
**Prossimo step:** Genera Token PAT e testa il push!
|
||||
@@ -1,305 +0,0 @@
|
||||
# ?? Sistema di Versionamento Automatico
|
||||
|
||||
## ?? Strategia Versioning
|
||||
|
||||
Il progetto AutoBidder segue **[Semantic Versioning 2.0.0](https://semver.org/)** nel formato:
|
||||
|
||||
```
|
||||
MAJOR.MINOR.PATCH
|
||||
```
|
||||
|
||||
### Quando Incrementare
|
||||
|
||||
| Tipo | Quando | Esempio |
|
||||
|------|--------|---------|
|
||||
| **MAJOR** | Breaking changes | `1.5.2` ? `2.0.0` |
|
||||
| **MINOR** | Nuove feature retrocompatibili | `1.5.2` ? `1.6.0` |
|
||||
| **PATCH** | Bug fix retrocompatibili | `1.5.2` ? `1.5.3` |
|
||||
|
||||
---
|
||||
|
||||
## ?? Workflow di Rilascio
|
||||
|
||||
### 1. Modifica Versione in `.csproj`
|
||||
|
||||
```xml
|
||||
<!-- AutoBidder.csproj -->
|
||||
<PropertyGroup>
|
||||
<!-- v1.1.0: Docker/Gitea publishing workflow + HTTPS fix -->
|
||||
<Version>1.1.0</Version>
|
||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
||||
<FileVersion>1.1.0.0</FileVersion>
|
||||
<InformationalVersion>1.1.0</InformationalVersion>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
**? Questa è la FONTE UNICA della versione!**
|
||||
|
||||
### 2. Aggiorna `Dockerfile` Labels
|
||||
|
||||
```dockerfile
|
||||
LABEL org.opencontainers.image.version="1.1.0"
|
||||
```
|
||||
|
||||
### 3. Documenta in `CHANGELOG.md`
|
||||
|
||||
```markdown
|
||||
## [1.1.0] - 2025-01-18
|
||||
|
||||
### ? Aggiunte
|
||||
- Pubblicazione automatica su Gitea
|
||||
- ...
|
||||
|
||||
### ?? Modifiche
|
||||
- Porta HTTP: 5000 ? 8080
|
||||
- ...
|
||||
```
|
||||
|
||||
### 4. Pubblica su Gitea
|
||||
|
||||
```bash
|
||||
# Da Visual Studio: Tasto destro ? Pubblica ? GiteaRegistry
|
||||
# Oppure da CLI:
|
||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
||||
```
|
||||
|
||||
**Risultato automatico:**
|
||||
- `gitea.../autobidder:latest` (aggiornato)
|
||||
- `gitea.../autobidder:1.1.0` (nuovo tag)
|
||||
|
||||
---
|
||||
|
||||
## ?? Storico Versioni
|
||||
|
||||
### v1.1.0 - Docker/Gitea Publishing Workflow (2025-01-18)
|
||||
|
||||
**Feature Principali:**
|
||||
- ? Pubblicazione automatica Gitea Container Registry
|
||||
- ? Versionamento automatico da `.csproj`
|
||||
- ?? HTTPS disabilitato di default in container
|
||||
- ?? Porta HTTP standardizzata (8080)
|
||||
- ?? Fix errore Visual Studio "ContainerBuild"
|
||||
- ?? Fix crash container certificati HTTPS
|
||||
|
||||
**Breaking Changes:**
|
||||
- ?? Porta: `5000` ? `8080`
|
||||
- ?? Path Gitea: `alby96/mimante/autobidder` ? `alby96/autobidder`
|
||||
- ?? HTTPS: abilitato ? disabilitato (opzionale)
|
||||
|
||||
**Migrazione:**
|
||||
```bash
|
||||
# Aggiorna port mapping
|
||||
docker run -p 5000:8080 ... # era 5000:5000
|
||||
|
||||
# Pull nuova convenzione path
|
||||
docker pull gitea.../alby96/autobidder:1.1.0
|
||||
```
|
||||
|
||||
### v1.0.0 - Release Iniziale (2025-01-17)
|
||||
|
||||
**Feature Principali:**
|
||||
- ? Sistema AutoBidder Blazor .NET 8
|
||||
- ? Monitoraggio aste Bidoo
|
||||
- ? Offerte automatiche
|
||||
- ? Statistiche PostgreSQL
|
||||
- ? Docker support base
|
||||
|
||||
---
|
||||
|
||||
## ?? Esempi Pratici
|
||||
|
||||
### Scenario 1: Bug Fix
|
||||
|
||||
**Situazione:** Corretto bug calcolo statistiche
|
||||
|
||||
```xml
|
||||
<!-- Prima -->
|
||||
<Version>1.1.0</Version>
|
||||
|
||||
<!-- Dopo -->
|
||||
<Version>1.1.1</Version>
|
||||
```
|
||||
|
||||
```markdown
|
||||
## [1.1.1] - 2025-01-19
|
||||
|
||||
### ?? Correzioni
|
||||
- Fix calcolo media offerte in Statistics.razor
|
||||
```
|
||||
|
||||
### Scenario 2: Nuova Feature
|
||||
|
||||
**Situazione:** Aggiunto supporto notifiche email
|
||||
|
||||
```xml
|
||||
<!-- Prima -->
|
||||
<Version>1.1.1</Version>
|
||||
|
||||
<!-- Dopo -->
|
||||
<Version>1.2.0</Version>
|
||||
```
|
||||
|
||||
```markdown
|
||||
## [1.2.0] - 2025-01-20
|
||||
|
||||
### ? Aggiunte
|
||||
- Notifiche email per aste vinte
|
||||
- Configurazione SMTP in Settings
|
||||
```
|
||||
|
||||
### Scenario 3: Breaking Change
|
||||
|
||||
**Situazione:** API REST completamente ristrutturata
|
||||
|
||||
```xml
|
||||
<!-- Prima -->
|
||||
<Version>1.2.0</Version>
|
||||
|
||||
<!-- Dopo -->
|
||||
<Version>2.0.0</Version>
|
||||
```
|
||||
|
||||
```markdown
|
||||
## [2.0.0] - 2025-02-01
|
||||
|
||||
### ?? BREAKING CHANGES
|
||||
- API REST ristrutturata (endpoints modificati)
|
||||
- Migrazione richiesta per client esistenti
|
||||
|
||||
### ?? Modifiche
|
||||
- Endpoint `/api/auctions` ? `/api/v2/auctions`
|
||||
- Response format JSON standardizzato
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Automazione
|
||||
|
||||
### GitHub Actions / Gitea Actions
|
||||
|
||||
```yaml
|
||||
# .gitea/workflows/version-check.yml
|
||||
name: Version Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, docker ]
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Extract version
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(grep -oP '<Version>\K[^<]+' AutoBidder.csproj | head -1)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check CHANGELOG
|
||||
run: |
|
||||
if ! grep -q "## \[${{ steps.version.outputs.version }}\]" CHANGELOG.md; then
|
||||
echo "?? Versione ${{ steps.version.outputs.version }} non documentata in CHANGELOG.md"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create Git Tag
|
||||
run: |
|
||||
git tag v${{ steps.version.outputs.version }}
|
||||
git push origin v${{ steps.version.outputs.version }}
|
||||
```
|
||||
|
||||
### PowerShell Script Locale
|
||||
|
||||
```powershell
|
||||
# scripts/bump-version.ps1
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateSet('major','minor','patch')]
|
||||
[string]$Type
|
||||
)
|
||||
|
||||
# Leggi versione corrente
|
||||
$csproj = "AutoBidder.csproj"
|
||||
$content = Get-Content $csproj -Raw
|
||||
$version = [regex]::Match($content, '<Version>(.*?)</Version>').Groups[1].Value
|
||||
|
||||
# Parse semantic version
|
||||
$parts = $version -split '\.'
|
||||
$major = [int]$parts[0]
|
||||
$minor = [int]$parts[1]
|
||||
$patch = [int]$parts[2]
|
||||
|
||||
# Incrementa
|
||||
switch ($Type) {
|
||||
'major' { $major++; $minor=0; $patch=0 }
|
||||
'minor' { $minor++; $patch=0 }
|
||||
'patch' { $patch++ }
|
||||
}
|
||||
|
||||
$newVersion = "$major.$minor.$patch"
|
||||
|
||||
# Aggiorna .csproj
|
||||
$content = $content -replace '<Version>.*?</Version>', "<Version>$newVersion</Version>"
|
||||
$content = $content -replace '<AssemblyVersion>.*?</AssemblyVersion>', "<AssemblyVersion>$newVersion.0</AssemblyVersion>"
|
||||
$content = $content -replace '<FileVersion>.*?</FileVersion>', "<FileVersion>$newVersion.0</FileVersion>"
|
||||
$content = $content -replace '<InformationalVersion>.*?</InformationalVersion>', "<InformationalVersion>$newVersion</InformationalVersion>"
|
||||
Set-Content $csproj $content
|
||||
|
||||
# Aggiorna Dockerfile
|
||||
$dockerfile = "Dockerfile"
|
||||
$dockerContent = Get-Content $dockerfile -Raw
|
||||
$dockerContent = $dockerContent -replace 'org.opencontainers.image.version=".*?"', "org.opencontainers.image.version=""$newVersion"""
|
||||
Set-Content $dockerfile $dockerContent
|
||||
|
||||
Write-Host "? Versione aggiornata: $version ? $newVersion"
|
||||
Write-Host "?? Ricorda di aggiornare CHANGELOG.md!"
|
||||
```
|
||||
|
||||
**Uso:**
|
||||
```powershell
|
||||
# Incrementa PATCH (bug fix)
|
||||
.\scripts\bump-version.ps1 -Type patch
|
||||
|
||||
# Incrementa MINOR (nuova feature)
|
||||
.\scripts\bump-version.ps1 -Type minor
|
||||
|
||||
# Incrementa MAJOR (breaking change)
|
||||
.\scripts\bump-version.ps1 -Type major
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Riferimenti
|
||||
|
||||
- [Semantic Versioning 2.0.0](https://semver.org/)
|
||||
- [Keep a Changelog](https://keepachangelog.com/)
|
||||
- [Conventional Commits](https://www.conventionalcommits.org/)
|
||||
- [GitVersion](https://gitversion.net/) (tool automatico)
|
||||
|
||||
---
|
||||
|
||||
## ? Checklist Release
|
||||
|
||||
Prima di ogni release:
|
||||
|
||||
- [ ] Versione incrementata in `AutoBidder.csproj`
|
||||
- [ ] Versione aggiornata in `Dockerfile` labels
|
||||
- [ ] Modifiche documentate in `CHANGELOG.md`
|
||||
- [ ] Build locale testata
|
||||
- [ ] Container Docker testato localmente
|
||||
- [ ] Pubblicazione su Gitea completata
|
||||
- [ ] Tag Git creato (`v1.1.0`)
|
||||
- [ ] Documentazione aggiornata (se necessario)
|
||||
|
||||
**Dopo la release:**
|
||||
|
||||
- [ ] Verifica immagine su Gitea
|
||||
- [ ] Test pull e deploy
|
||||
- [ ] Comunicazione team (se applicabile)
|
||||
- [ ] Aggiornamento deployment production
|
||||
|
||||
---
|
||||
|
||||
**?? Versione corrente:** `1.1.0` - Docker/Gitea Publishing Workflow
|
||||
@@ -1,340 +0,0 @@
|
||||
# ?? SISTEMA VERSIONAMENTO IMPLEMENTATO
|
||||
|
||||
## ? Versione Corrente: `1.1.0`
|
||||
|
||||
**Data:** 2025-01-18
|
||||
**Tipo:** MINOR (nuove feature + bug fix)
|
||||
**Modifiche:** Docker/Gitea Publishing Workflow + HTTPS Fix
|
||||
|
||||
---
|
||||
|
||||
## ?? File Creati/Aggiornati
|
||||
|
||||
### Nuovi File
|
||||
|
||||
1. **`CHANGELOG.md`**
|
||||
- Storico completo modifiche
|
||||
- Formato [Keep a Changelog](https://keepachangelog.com/)
|
||||
- Documentazione v1.1.0 completa
|
||||
|
||||
2. **`VERSIONING.md`**
|
||||
- Guida sistema versionamento
|
||||
- Workflow di rilascio
|
||||
- Esempi pratici
|
||||
- Automazione
|
||||
|
||||
3. **`bump-version.ps1`**
|
||||
- Script PowerShell automatico
|
||||
- Incrementa MAJOR/MINOR/PATCH
|
||||
- Aggiorna tutti i file coinvolti
|
||||
- Genera template CHANGELOG
|
||||
|
||||
### File Aggiornati
|
||||
|
||||
1. **`AutoBidder.csproj`**
|
||||
```xml
|
||||
<!-- v1.1.0: Docker/Gitea publishing workflow + HTTPS fix -->
|
||||
<Version>1.1.0</Version>
|
||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
||||
<FileVersion>1.1.0.0</FileVersion>
|
||||
<InformationalVersion>1.1.0</InformationalVersion>
|
||||
```
|
||||
|
||||
2. **`Dockerfile`**
|
||||
```dockerfile
|
||||
LABEL org.opencontainers.image.version="1.1.0"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Come Usare il Sistema
|
||||
|
||||
### Metodo 1: Script Automatico (CONSIGLIATO)
|
||||
|
||||
```powershell
|
||||
# Bug fix (1.1.0 ? 1.1.1)
|
||||
.\bump-version.ps1 -Type patch
|
||||
|
||||
# Nuova feature (1.1.0 ? 1.2.0)
|
||||
.\bump-version.ps1 -Type minor
|
||||
|
||||
# Breaking change (1.1.0 ? 2.0.0)
|
||||
.\bump-version.ps1 -Type major
|
||||
```
|
||||
|
||||
**Lo script fa automaticamente:**
|
||||
1. ? Incrementa versione in `AutoBidder.csproj`
|
||||
2. ? Aggiorna `Dockerfile` labels
|
||||
3. ? Aggiunge template in `CHANGELOG.md`
|
||||
4. ? Mostra prossimi passi
|
||||
|
||||
### Metodo 2: Manuale
|
||||
|
||||
1. **Modifica `AutoBidder.csproj`:**
|
||||
```xml
|
||||
<Version>1.2.0</Version>
|
||||
```
|
||||
|
||||
2. **Modifica `Dockerfile`:**
|
||||
```dockerfile
|
||||
LABEL org.opencontainers.image.version="1.2.0"
|
||||
```
|
||||
|
||||
3. **Aggiorna `CHANGELOG.md`:**
|
||||
```markdown
|
||||
## [1.2.0] - 2025-01-19
|
||||
|
||||
### ? Aggiunte
|
||||
- Nuova feature X
|
||||
```
|
||||
|
||||
4. **Pubblica:**
|
||||
```bash
|
||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Workflow Completo di Rilascio
|
||||
|
||||
### Step 1: Incrementa Versione
|
||||
|
||||
```powershell
|
||||
.\bump-version.ps1 -Type minor
|
||||
```
|
||||
|
||||
### Step 2: Compila CHANGELOG
|
||||
|
||||
Apri `CHANGELOG.md` e completa il template:
|
||||
|
||||
```markdown
|
||||
## [1.2.0] - 2025-01-19
|
||||
|
||||
### ? Aggiunte
|
||||
- Feature notifiche email per aste vinte
|
||||
- Configurazione SMTP in Settings
|
||||
|
||||
### ?? Modifiche
|
||||
- Migliorato algoritmo calcolo statistiche
|
||||
|
||||
### ?? Correzioni
|
||||
- Fix bug crash su asta annullata
|
||||
```
|
||||
|
||||
### Step 3: Commit Modifiche
|
||||
|
||||
```bash
|
||||
git add AutoBidder.csproj Dockerfile CHANGELOG.md
|
||||
git commit -m "chore: bump version to v1.2.0
|
||||
|
||||
- Feature notifiche email
|
||||
- Fix bug crash asta annullata"
|
||||
```
|
||||
|
||||
### Step 4: Tag Git
|
||||
|
||||
```bash
|
||||
git tag v1.2.0
|
||||
git push origin docker --tags
|
||||
```
|
||||
|
||||
### Step 5: Pubblica Docker su Gitea
|
||||
|
||||
**Da Visual Studio:**
|
||||
- Tasto destro ? Pubblica ? GiteaRegistry
|
||||
|
||||
**Da CLI:**
|
||||
```bash
|
||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
||||
```
|
||||
|
||||
### Step 6: Verifica Pubblicazione
|
||||
|
||||
```bash
|
||||
# Controlla su Gitea
|
||||
https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder
|
||||
|
||||
# Verifica tag creati
|
||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:latest
|
||||
docker pull gitea.encke-hake.ts.net/alby96/autobidder:1.2.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Semantic Versioning
|
||||
|
||||
| Versione | Tipo | Quando Usare | Esempio |
|
||||
|----------|------|--------------|---------|
|
||||
| **1.0.0 ? 2.0.0** | MAJOR | Breaking changes | API cambiata, porta diversa |
|
||||
| **1.0.0 ? 1.1.0** | MINOR | Nuove feature | Notifiche email, esportazione dati |
|
||||
| **1.0.0 ? 1.0.1** | PATCH | Bug fix | Fix crash, correzione calcoli |
|
||||
|
||||
### Esempi Pratici
|
||||
|
||||
**Bug Fix (PATCH):**
|
||||
```powershell
|
||||
.\bump-version.ps1 -Type patch
|
||||
# 1.1.0 ? 1.1.1
|
||||
```
|
||||
|
||||
**Nuova Feature (MINOR):**
|
||||
```powershell
|
||||
.\bump-version.ps1 -Type minor
|
||||
# 1.1.1 ? 1.2.0
|
||||
```
|
||||
|
||||
**Breaking Change (MAJOR):**
|
||||
```powershell
|
||||
.\bump-version.ps1 -Type major
|
||||
# 1.2.0 ? 2.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Tag Docker Generati
|
||||
|
||||
### Dopo Pubblicazione v1.1.0
|
||||
|
||||
```bash
|
||||
# Tag su Gitea
|
||||
gitea.encke-hake.ts.net/alby96/autobidder:latest ? v1.1.0
|
||||
gitea.encke-hake.ts.net/alby96/autobidder:1.1.0 ? immutabile
|
||||
gitea.encke-hake.ts.net/alby96/autobidder:1.0.0 ? ancora disponibile
|
||||
```
|
||||
|
||||
### Production Best Practice
|
||||
|
||||
**? NON USARE `latest` in production:**
|
||||
```yaml
|
||||
# ERRATO
|
||||
image: gitea.../autobidder:latest
|
||||
```
|
||||
|
||||
**? USA versione specifica:**
|
||||
```yaml
|
||||
# CORRETTO
|
||||
image: gitea.../autobidder:1.1.0
|
||||
```
|
||||
|
||||
**Motivo:** `latest` cambia ad ogni release, versione specifica è immutabile.
|
||||
|
||||
---
|
||||
|
||||
## ?? Gestione Hotfix
|
||||
|
||||
### Scenario: Bug critico in production
|
||||
|
||||
**Production usa:** `v1.1.0`
|
||||
**Development è a:** `v1.2.0-dev`
|
||||
|
||||
**Workflow:**
|
||||
|
||||
1. **Crea branch hotfix:**
|
||||
```bash
|
||||
git checkout -b hotfix/1.1.1 v1.1.0
|
||||
```
|
||||
|
||||
2. **Applica fix:**
|
||||
```bash
|
||||
# Fix bug
|
||||
.\bump-version.ps1 -Type patch # 1.1.0 ? 1.1.1
|
||||
```
|
||||
|
||||
3. **Pubblica hotfix:**
|
||||
```bash
|
||||
git commit -m "fix: critical bug in auction monitoring"
|
||||
git tag v1.1.1
|
||||
git push origin hotfix/1.1.1 --tags
|
||||
dotnet publish /p:PublishProfile=GiteaRegistry
|
||||
```
|
||||
|
||||
4. **Merge in main:**
|
||||
```bash
|
||||
git checkout docker
|
||||
git merge hotfix/1.1.1
|
||||
```
|
||||
|
||||
5. **Aggiorna development:**
|
||||
```bash
|
||||
# Se necessario, cherry-pick il fix in v1.2.0-dev
|
||||
git cherry-pick <commit-hash>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Dashboard Versioni
|
||||
|
||||
### Versioni Attive
|
||||
|
||||
| Versione | Stato | Tag Docker | Ambiente |
|
||||
|----------|-------|------------|----------|
|
||||
| `1.1.0` | ? Latest | `latest`, `1.1.0` | Production |
|
||||
| `1.0.0` | ?? Deprecated | `1.0.0` | Legacy |
|
||||
|
||||
### Roadmap
|
||||
|
||||
| Versione | Tipo | Piano | Data Target |
|
||||
|----------|------|-------|-------------|
|
||||
| `1.2.0` | MINOR | Notifiche email | Feb 2025 |
|
||||
| `1.3.0` | MINOR | API REST | Mar 2025 |
|
||||
| `2.0.0` | MAJOR | Refactor architettura | Q2 2025 |
|
||||
|
||||
---
|
||||
|
||||
## ? Checklist Release
|
||||
|
||||
Prima di ogni release:
|
||||
|
||||
- [ ] **Versione incrementata** in `AutoBidder.csproj`
|
||||
- [ ] **Versione aggiornata** in `Dockerfile`
|
||||
- [ ] **CHANGELOG.md** compilato con modifiche
|
||||
- [ ] **Build locale** testata
|
||||
- [ ] **Container Docker** testato localmente
|
||||
- [ ] **Pubblicazione Gitea** completata
|
||||
- [ ] **Tag Git** creato (`v1.1.0`)
|
||||
- [ ] **Documentazione** aggiornata (se necessario)
|
||||
- [ ] **Migration guide** scritta (per breaking changes)
|
||||
- [ ] **Communication** team/utenti (se applicabile)
|
||||
|
||||
Dopo la release:
|
||||
|
||||
- [ ] **Verifica immagine** su Gitea
|
||||
- [ ] **Test pull** e deploy
|
||||
- [ ] **Monitoraggio** errori prime 24h
|
||||
- [ ] **Aggiornamento** deployment production
|
||||
|
||||
---
|
||||
|
||||
## ?? Benefici del Sistema
|
||||
|
||||
### Prima (senza versioning)
|
||||
- ? Versioni non tracciate
|
||||
- ? Modifiche non documentate
|
||||
- ? Impossibile rollback a versione specifica
|
||||
- ? Difficile capire cosa è cambiato
|
||||
|
||||
### Dopo (con versioning)
|
||||
- ? Ogni modifica tracciata con versione
|
||||
- ? CHANGELOG completo e leggibile
|
||||
- ? Rollback facile (`docker pull .../:1.0.0`)
|
||||
- ? Deploy controllati e verificabili
|
||||
- ? Automazione con script PowerShell
|
||||
- ? Tag Docker immutabili per production
|
||||
|
||||
---
|
||||
|
||||
## ?? Documenti di Riferimento
|
||||
|
||||
| File | Scopo |
|
||||
|------|-------|
|
||||
| `CHANGELOG.md` | Storico modifiche per utenti |
|
||||
| `VERSIONING.md` | Guida sistema per sviluppatori |
|
||||
| `bump-version.ps1` | Automazione incremento versione |
|
||||
| `AutoBidder.csproj` | Fonte unica della verità (versione) |
|
||||
| `Dockerfile` | Metadata versione immagine |
|
||||
|
||||
---
|
||||
|
||||
**?? Versione attuale: `1.1.0` - Docker/Gitea Publishing Workflow**
|
||||
|
||||
**? Sistema di versionamento completamente implementato e operativo!**
|
||||
+10
-43
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -76,6 +76,7 @@
|
||||
window.Blazor.addEventListener('enhancedload', initLogScroll);
|
||||
}
|
||||
|
||||
|
||||
// Esporta funzione per forzare scroll
|
||||
window.forceLogScrollToBottom = function () {
|
||||
logBoxes.forEach(logBox => {
|
||||
@@ -83,4 +84,18 @@
|
||||
scrollToBottom(logBox);
|
||||
});
|
||||
};
|
||||
|
||||
// Funzione chiamabile da Blazor per scroll specifico elemento
|
||||
window.scrollToBottom = function (elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
// Controlla se siamo già in fondo o quasi (entro 100px)
|
||||
const isNearBottom = element.scrollHeight - element.scrollTop - element.clientHeight < 100;
|
||||
|
||||
// Auto-scroll solo se siamo già in fondo (non interrompe lettura manuale)
|
||||
if (isNearBottom || !userScrolling.get(element)) {
|
||||
element.scrollTop = element.scrollHeight;
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user