Refactoring: Docker, CI/CD, tema WPF, DB avanzato, UX

- Aggiunto sistema completo di build/deploy Docker, Makefile, compose, .env, workflow CI/CD (Gitea, GitHub Actions)
- Nuovo servizio DatabaseService con migrations, healthcheck, backup, ottimizzazione, info
- Endpoint /health per healthcheck container
- Impostazioni avanzate di avvio aste (ricorda stato, auto-start, default nuove aste)
- Nuovo tema grafico WPF: palette, sidebar, layout griglia, log colorati, badge, cards, modali, responsività
- Migliorato calcolo valore prodotto, logica convenienza, blocco puntate non convenienti, log dettagliati
- Semplificate e migliorate pagine FreeBids, Settings, Statistics; rimossa Browser.razor
- Aggiornato .gitignore, documentazione, struttura progetto
- Base solida per future funzionalità avanzate e deploy professionale
This commit is contained in:
2025-12-23 21:35:44 +01:00
parent 009fa51155
commit 29724f5baf
32 changed files with 3761 additions and 1480 deletions

View File

@@ -1,28 +1,87 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
Documentation/
.github/
**Dockerignore file**
# Build artifacts
**/bin/
**/obj/
**/out/
**/publish/
# User-specific files
*.user
*.suo
*.userosscache
*.sln.docstates
# IDE files
.vs/
.vscode/
.idea/
*.swp
*.swo
*~
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# NuGet packages
*.nupkg
*.snupkg
**/packages/*
!**/packages/build/
# Test results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# Data and databases (exclude from image)
**/data/*.db
**/data/*.db-shm
**/data/*.db-wal
**/data/backups/
**/data/logs/
# Git files
.git
.gitignore
.gitattributes
.github/
# CI/CD files
.gitea/
# Documentation
*.md
!README.md
# Docker files
Dockerfile*
docker-compose*
.dockerignore
# Environment files
.env
.env.*
# Temporary files
*.tmp
*.temp
*.cache
*.bak
*.log
# OS files
.DS_Store
Thumbs.db

71
Mimante/.env.example Normal file
View File

@@ -0,0 +1,71 @@
# AutoBidder Environment Variables
# Copia questo file in .env e configura i valori
# === ASP.NET Core Configuration ===
ASPNETCORE_ENVIRONMENT=Production
ASPNETCORE_URLS=http://+:5000;https://+:5001
# === HTTPS Certificate ===
# Password per il certificato PFX
CERT_PASSWORD=AutoBidder2024
# === 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
# === Database Configuration ===
# Path database (default: /app/data/autobidder.db in container)
# DATABASE_PATH=/app/data/autobidder.db
# === Logging ===
# Livello log: Trace, Debug, Information, Warning, Error, Critical
# LOG_LEVEL=Information
# === Application Settings ===
# Numero massimo connessioni concorrenti HTTP
# MAX_HTTP_CONNECTIONS=10
# Timeout richieste HTTP (secondi)
# HTTP_TIMEOUT=30
# === Backup Configuration ===
# Directory backup (default: /app/data/backups)
# BACKUP_DIR=/app/data/backups
# Numero giorni backup da mantenere
# BACKUP_RETENTION_DAYS=30
# === Security ===
# Chiave segreta per DataProtection (genera random se non specificato)
# DATA_PROTECTION_KEY=your-random-key-here
# === Monitoring ===
# Abilita metriche Prometheus (true/false)
# ENABLE_METRICS=false
# Porta metriche (se ENABLE_METRICS=true)
# METRICS_PORT=9090
# === Advanced ===
# Numero thread worker per polling aste
# AUCTION_WORKER_THREADS=4
# Intervallo pulizia cache (minuti)
# CACHE_CLEANUP_INTERVAL=60

View File

@@ -0,0 +1,89 @@
name: Build and Deploy AutoBidder
on:
push:
branches:
- main
- docker
pull_request:
branches:
- main
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
continue-on-error: true
- name: Publish
run: dotnet publish --configuration Release --no-build --output ./publish
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Gitea Container Registry
uses: docker/login-action@v2
with:
registry: ${{ secrets.GITEA_REGISTRY }}
username: ${{ secrets.GITEA_USERNAME }}
password: ${{ secrets.GITEA_PASSWORD }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ secrets.GITEA_REGISTRY }}/autobidder
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.GITEA_REGISTRY }}/autobidder:buildcache
cache-to: type=registry,ref=${{ secrets.GITEA_REGISTRY }}/autobidder:buildcache,mode=max
deploy:
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/docker'
steps:
- name: Deploy to server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
cd /opt/autobidder
docker-compose pull
docker-compose down
docker-compose up -d
docker-compose logs -f --tail=50

97
Mimante/.github/workflows/ci-cd.yml vendored Normal file
View File

@@ -0,0 +1,97 @@
name: AutoBidder CI/CD
on:
push:
branches: [ main, docker ]
pull_request:
branches: [ main ]
env:
REGISTRY: 192.168.30.23/Alby96
IMAGE_NAME: autobidder
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
continue-on-error: true
- name: Publish
run: dotnet publish --configuration Release --no-build --output ./publish
docker-build-push:
needs: build-and-test
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Gitea Registry
uses: docker/login-action@v2
with:
registry: ${{ secrets.GITEA_REGISTRY }}
username: ${{ secrets.GITEA_USERNAME }}
password: ${{ secrets.GITEA_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
deploy:
needs: docker-build-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/docker' || github.ref == 'refs/heads/main'
steps:
- name: Deploy to server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
cd /opt/autobidder
source .env
echo "$GITEA_PASSWORD" | docker login $GITEA_REGISTRY -u $GITEA_USERNAME --password-stdin
docker-compose pull
docker-compose down
docker-compose up -d
sleep 10
docker-compose ps
docker-compose logs --tail=30

456
Mimante/.gitignore vendored Normal file
View File

@@ -0,0 +1,456 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
.idea/
# ============================================
# AutoBidder Specific
# ============================================
# Database files (local development)
*.db
*.db-shm
*.db-wal
data/*.db
data/*.db-*
# Backups (keep structure, ignore files)
data/backups/*.db
data/backups/*.json
# Logs
logs/*.log
logs/*.txt
*.log
# Environment files with secrets
.env
.env.local
.env.*.local
# Certificates and keys
*.pfx
*.key
*.crt
*.pem
cert/*
!cert/.gitkeep
# Docker volumes data
test-data/
# Published artifacts
publish/
PublishProfiles/
# Temp directories
temp/
tmp/
# OS files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Keep important empty directories
!data/.gitkeep
!data/backups/.gitkeep
!logs/.gitkeep
!cert/.gitkeep

View File

@@ -59,4 +59,13 @@
</Content>
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\js\" />
</ItemGroup>
<ItemGroup>
<None Include=".gitea\workflows\deploy.yml" />
<None Include=".github\workflows\ci-cd.yml" />
</ItemGroup>
</Project>

1
Mimante/Data/.gitkeep Normal file
View File

@@ -0,0 +1 @@
# This file ensures the data directory is tracked by git

View File

@@ -1,21 +1,69 @@
# Usa l'immagine base ASP.NET Runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 5000
# Usa l'immagine SDK per build
# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copia solo i file di progetto per cache layer restore
COPY ["AutoBidder.csproj", "./"]
RUN dotnet restore "AutoBidder.csproj"
# Copia tutto il codice sorgente
COPY . .
RUN dotnet build "AutoBidder.csproj" -c Release -o /app/build
# Build con ottimizzazioni
RUN dotnet build "AutoBidder.csproj" \
-c Release \
-o /app/build \
--no-restore
# Stage 2: Publish
FROM build AS publish
RUN dotnet publish "AutoBidder.csproj" -c Release -o /app/publish /p:UseAppHost=false
RUN dotnet publish "AutoBidder.csproj" \
-c Release \
-o /app/publish \
--no-restore \
--no-build \
/p:UseAppHost=false \
/p:PublishTrimmed=false \
/p:PublishSingleFile=false
# Immagine finale
FROM base AS final
# Stage 3: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
# Installa curl per healthcheck e tools utili
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
ca-certificates \
sqlite3 && \
rm -rf /var/lib/apt/lists/*
# Crea directory per dati e certificati
RUN mkdir -p /app/data /app/data/backups /app/cert /app/logs && \
chmod 755 /app/data /app/cert /app/logs
# Copia artifacts da publish stage
COPY --from=publish /app/publish .
# Esponi porte
EXPOSE 5000
EXPOSE 5001
# Healthcheck
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:5000/health || exit 1
# User non-root per sicurezza
RUN useradd -m -u 1000 appuser && \
chown -R appuser:appuser /app
USER appuser
# Labels per metadata
LABEL org.opencontainers.image.title="AutoBidder" \
org.opencontainers.image.description="Sistema automatizzato di gestione aste Blazor" \
org.opencontainers.image.version="1.0.0" \
org.opencontainers.image.vendor="Alby96" \
org.opencontainers.image.source="https://192.168.30.23/Alby96/Mimante"
# Entrypoint
ENTRYPOINT ["dotnet", "AutoBidder.dll"]

186
Mimante/Makefile Normal file
View File

@@ -0,0 +1,186 @@
.PHONY: help build run stop logs clean deploy backup test
# Variables
IMAGE_NAME := autobidder
CONTAINER_NAME := autobidder
REGISTRY := 192.168.30.23/Alby96
TAG := latest
COMPOSE_FILE := docker-compose.yml
# Colors
GREEN := \033[0;32m
YELLOW := \033[1;33m
NC := \033[0m
help: ## Mostra questo messaggio di aiuto
@echo "$(GREEN)AutoBidder - Makefile Commands$(NC)"
@echo "================================"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(YELLOW)%-15s$(NC) %s\n", $$1, $$2}'
build: ## Build Docker image
@echo "$(GREEN)Building Docker image...$(NC)"
docker build -t $(IMAGE_NAME):$(TAG) .
build-no-cache: ## Build Docker image senza cache
@echo "$(GREEN)Building Docker image (no cache)...$(NC)"
docker build --no-cache -t $(IMAGE_NAME):$(TAG) .
run: ## Avvia container con docker-compose
@echo "$(GREEN)Starting containers...$(NC)"
docker-compose -f $(COMPOSE_FILE) up -d
stop: ## Ferma container
@echo "$(GREEN)Stopping containers...$(NC)"
docker-compose -f $(COMPOSE_FILE) down
restart: stop run ## Restart container
logs: ## Mostra logs real-time
docker-compose -f $(COMPOSE_FILE) logs -f
logs-tail: ## Mostra ultimi 100 logs
docker-compose -f $(COMPOSE_FILE) logs --tail=100
ps: ## Mostra status container
docker-compose -f $(COMPOSE_FILE) ps
shell: ## Apri shell nel container
docker-compose -f $(COMPOSE_FILE) exec autobidder /bin/bash
clean: ## Pulisci container, immagini e volumi
@echo "$(GREEN)Cleaning up...$(NC)"
docker-compose -f $(COMPOSE_FILE) down -v
docker rmi $(IMAGE_NAME):$(TAG) 2>/dev/null || true
clean-all: ## Pulisci tutto inclusi dati (PERICOLOSO!)
@echo "$(YELLOW)Warning: This will delete all data!$(NC)"
@read -p "Are you sure? [y/N] " -n 1 -r; \
echo; \
if [[ $$REPLY =~ ^[Yy]$$ ]]; then \
chmod +x scripts/clean.sh; \
./scripts/clean.sh; \
fi
clean-build: ## Pulisci solo build artifacts
@echo "$(GREEN)Cleaning build artifacts...$(NC)"
dotnet clean
rm -rf bin/ obj/ publish/
tag: ## Tag immagine per registry
@echo "$(GREEN)Tagging image for registry...$(NC)"
docker tag $(IMAGE_NAME):$(TAG) $(REGISTRY)/$(IMAGE_NAME):$(TAG)
push: tag ## Push immagine su registry
@echo "$(GREEN)Pushing image to registry...$(NC)"
docker push $(REGISTRY)/$(IMAGE_NAME):$(TAG)
pull: ## Pull immagine da registry
@echo "$(GREEN)Pulling image from registry...$(NC)"
docker pull $(REGISTRY)/$(IMAGE_NAME):$(TAG)
login: ## Login al registry Gitea
@echo "$(GREEN)Logging in to registry...$(NC)"
@read -p "Username: " username; \
docker login $(REGISTRY) -u $$username
deploy: pull run ## Deploy (pull + run)
@echo "$(GREEN)Deployment completed!$(NC)"
backup: ## Backup database
@echo "$(GREEN)Creating backup...$(NC)"
@mkdir -p ./data/backups
@if [ -f ./data/autobidder.db ]; then \
cp ./data/autobidder.db ./data/backups/autobidder_$$(date +%Y%m%d_%H%M%S).db; \
echo "$(GREEN)Backup created$(NC)"; \
else \
echo "$(YELLOW)No database found$(NC)"; \
fi
restore: ## Restore database da backup
@echo "$(GREEN)Available backups:$(NC)"
@ls -1 ./data/backups/*.db 2>/dev/null || echo "No backups found"
@read -p "Enter backup filename: " backup; \
if [ -f ./data/backups/$$backup ]; then \
cp ./data/backups/$$backup ./data/autobidder.db; \
echo "$(GREEN)Database restored$(NC)"; \
docker-compose restart; \
else \
echo "$(YELLOW)Backup not found$(NC)"; \
fi
test: ## Test build locale
@echo "$(GREEN)Testing build...$(NC)"
dotnet build -c Release
dotnet test --no-build
publish: ## Publish artifacts locali
@echo "$(GREEN)Publishing artifacts...$(NC)"
dotnet publish -c Release -o ./publish
dev: ## Avvia in modalità development
@echo "$(GREEN)Starting in development mode...$(NC)"
dotnet run
dev-docker: ## Avvia con Docker Compose dev
@echo "$(GREEN)Starting with Docker Compose (dev)...$(NC)"
docker-compose -f docker-compose.dev.yml up
dev-docker-debug: ## Avvia con Docker Compose + SQLite browser
@echo "$(GREEN)Starting with Docker Compose + SQLite browser...${NC}"
docker-compose -f docker-compose.dev.yml --profile debug up
watch: ## Avvia con hot-reload
@echo "$(GREEN)Starting with hot-reload...$(NC)"
dotnet watch run
test-docker: ## Test Docker build locale
@echo "$(GREEN)Testing Docker build...$(NC)"
@chmod +x scripts/test-docker.sh
@./scripts/test-docker.sh
health: ## Verifica health container
@echo "$(GREEN)Checking container health...$(NC)"
@docker inspect --format='{{.State.Health.Status}}' $(CONTAINER_NAME) 2>/dev/null || echo "Container not running"
stats: ## Mostra statistiche container
docker stats $(CONTAINER_NAME) --no-stream
db-info: ## Mostra info database
@echo "$(GREEN)Database information:$(NC)"
@if [ -f ./data/autobidder.db ]; then \
ls -lh ./data/autobidder.db; \
echo "Backups:"; \
ls -lh ./data/backups/*.db 2>/dev/null | tail -5 || echo "No backups"; \
else \
echo "$(YELLOW)No database found$(NC)"; \
fi
optimize: ## Ottimizza database (VACUUM)
@echo "$(GREEN)Optimizing database...$(NC)"
docker-compose exec autobidder sqlite3 /app/data/autobidder.db "VACUUM;"
@echo "$(GREEN)Database optimized$(NC)"
update: ## Update immagine e restart
@echo "$(GREEN)Updating AutoBidder...$(NC)"
docker-compose pull
docker-compose up -d
@echo "$(GREEN)Update completed$(NC)"
version: ## Mostra versione
@echo "$(GREEN)AutoBidder Version:$(NC)"
@cat VERSION 2>/dev/null || echo "VERSION file not found"
@docker-compose exec autobidder dotnet AutoBidder.dll --version 2>/dev/null || echo "Container not running"
release: ## Crea nuova release (interattivo)
@echo "$(GREEN)Creating new release...$(NC)"
@chmod +x scripts/release.sh
@./scripts/release.sh
setup: ## Setup ambiente sviluppo
@echo "$(GREEN)Setting up development environment...$(NC)"
dotnet restore
@mkdir -p ./data ./data/backups ./cert ./logs
@echo "$(GREEN)Setup completed$(NC)"
ci: build test ## Esegui CI pipeline locale
@echo "$(GREEN)CI pipeline completed$(NC)"

View File

@@ -115,6 +115,12 @@ namespace AutoBidder.Models
/// </summary>
[JsonIgnore]
public ProductValue? CalculatedValue { get; set; }
/// <summary>
/// Ultimo stato ricevuto dal monitor per questa asta
/// </summary>
[JsonIgnore]
public AuctionState? LastState { get; set; }
/// <summary>
/// Aggiunge una voce al log dell'asta con limite automatico di righe

View File

@@ -1,292 +0,0 @@
@page "/browser"
@inject IJSRuntime JSRuntime
<PageTitle>Browser - AutoBidder</PageTitle>
<div class="browser-container animate-fade-in p-4">
<div class="d-flex align-items-center justify-content-between mb-4 animate-fade-in-down">
<div class="d-flex align-items-center">
<i class="bi bi-globe text-primary me-3" style="font-size: 2.5rem;"></i>
<h2 class="mb-0 fw-bold">Browser Integrato</h2>
</div>
<div class="btn-group">
<button class="btn btn-sm btn-secondary hover-lift" @onclick="NavigateBack" disabled="@(!canGoBack)">
<i class="bi bi-arrow-left"></i> Indietro
</button>
<button class="btn btn-sm btn-secondary hover-lift" @onclick="NavigateForward" disabled="@(!canGoForward)">
<i class="bi bi-arrow-right"></i> Avanti
</button>
<button class="btn btn-sm btn-primary hover-lift" @onclick="RefreshPage">
<i class="bi bi-arrow-clockwise"></i> Ricarica
</button>
</div>
</div>
<div class="browser-toolbar mb-3 animate-fade-in-up delay-100">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-link-45deg"></i>
</span>
<input type="text" class="form-control" @bind="currentUrl" @bind:event="oninput"
placeholder="https://it.bidoo.com" readonly />
<button class="btn btn-success hover-lift" @onclick="NavigateToBidoo">
<i class="bi bi-box-arrow-up-right"></i> Apri Bidoo
</button>
<button class="btn btn-info hover-lift" @onclick="ExtractCookie">
<i class="bi bi-cookie"></i> Estrai Cookie
</button>
</div>
</div>
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="alert alert-warning border-0 shadow-sm animate-shake mb-3">
<div class="d-flex align-items-center">
<i class="bi bi-exclamation-triangle-fill me-3"></i>
<div>
<strong>Limitazione Browser:</strong> @errorMessage
</div>
</div>
</div>
}
@if (!string.IsNullOrEmpty(extractedCookie))
{
<div class="alert alert-success border-0 shadow-sm animate-scale-in mb-3">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center flex-grow-1">
<i class="bi bi-check-circle-fill me-3" style="font-size: 1.5rem;"></i>
<div class="flex-grow-1">
<strong>Cookie Estratto:</strong>
<div class="mt-2">
<textarea class="form-control" rows="3" readonly>@extractedCookie</textarea>
</div>
</div>
</div>
<div class="ms-3">
<button class="btn btn-primary hover-lift" @onclick="CopyCookie">
<i class="bi bi-clipboard"></i> Copia
</button>
<a href="/settings" class="btn btn-success hover-lift ms-2">
<i class="bi bi-gear"></i> Vai a Impostazioni
</a>
</div>
</div>
</div>
}
<div class="browser-frame-container animate-fade-in-up delay-200">
<iframe id="bidooFrame"
src="@iframeUrl"
class="browser-frame"
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
@ref="iframeElement">
</iframe>
</div>
<div class="alert alert-info border-0 shadow-sm mt-3 animate-fade-in-up delay-300">
<div class="d-flex align-items-start">
<i class="bi bi-info-circle-fill me-3 mt-1" style="font-size: 1.5rem;"></i>
<div>
<h6 class="fw-bold mb-2">?? Come Usare il Browser Integrato</h6>
<ol class="mb-0">
<li>Clicca su <strong>"Apri Bidoo"</strong> per caricare il sito</li>
<li>Effettua il login con le tue credenziali Bidoo</li>
<li>Clicca su <strong>"Estrai Cookie"</strong> per recuperare il cookie di sessione</li>
<li>Il cookie verrà copiato automaticamente negli appunti</li>
<li>Vai alla pagina <strong>Impostazioni</strong> e incollalo nella sezione "Sessione Bidoo"</li>
</ol>
<div class="alert alert-warning border-0 mt-2 mb-0">
<small>
<i class="bi bi-exclamation-triangle"></i>
<strong>Nota:</strong> A causa delle limitazioni CORS, il cookie potrebbe non essere accessibile automaticamente.
In tal caso, usa gli strumenti sviluppatore del browser (F12) per estrarlo manualmente.
</small>
</div>
</div>
</div>
</div>
<div class="card border-secondary mt-4 animate-fade-in-up delay-400">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0"><i class="bi bi-tools"></i> Metodo Alternativo (Consigliato)</h5>
</div>
<div class="card-body">
<p>Se l'iframe non funziona correttamente, usa questo metodo:</p>
<div class="steps-list">
<div class="step-item hover-lift">
<div class="step-number">
<i class="bi bi-1-circle-fill"></i>
</div>
<div class="step-content">
<h6 class="fw-bold">Apri Bidoo in una Nuova Scheda</h6>
<a href="https://it.bidoo.com" target="_blank" class="btn btn-primary hover-lift mt-2">
<i class="bi bi-box-arrow-up-right"></i> https://it.bidoo.com
</a>
</div>
</div>
<div class="step-item hover-lift">
<div class="step-number">
<i class="bi bi-2-circle-fill"></i>
</div>
<div class="step-content">
<h6 class="fw-bold">Estrai il Cookie Manualmente</h6>
<p class="mb-0">Usa F12 ? Application/Storage ? Cookies ? bidoo.com ? Copia <code>__stattrb</code></p>
</div>
</div>
<div class="step-item hover-lift">
<div class="step-number">
<i class="bi bi-3-circle-fill"></i>
</div>
<div class="step-content">
<h6 class="fw-bold">Incolla nelle Impostazioni</h6>
<a href="/settings" class="btn btn-success hover-lift mt-2">
<i class="bi bi-gear-fill"></i> Vai alle Impostazioni
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.browser-container {
max-width: 1600px;
margin: 0 auto;
}
.browser-frame-container {
position: relative;
width: 100%;
height: 600px;
border: 2px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
background: var(--bg-secondary);
box-shadow: var(--shadow-lg);
}
.browser-frame {
width: 100%;
height: 100%;
border: none;
background: white;
}
.browser-toolbar {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 0.75rem;
}
</style>
@code {
private ElementReference iframeElement;
private string currentUrl = "about:blank";
private string iframeUrl = "about:blank";
private bool canGoBack = false;
private bool canGoForward = false;
private string? errorMessage = null;
private string? extractedCookie = null;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Inizializza listener iframe
await JSRuntime.InvokeVoidAsync("initializeBrowserFrame");
}
}
private async Task NavigateToBidoo()
{
try
{
iframeUrl = "https://it.bidoo.com";
currentUrl = iframeUrl;
errorMessage = null;
StateHasChanged();
}
catch (Exception ex)
{
errorMessage = $"Errore durante la navigazione: {ex.Message}";
}
}
private async Task NavigateBack()
{
try
{
await JSRuntime.InvokeVoidAsync("navigateBack");
canGoBack = await JSRuntime.InvokeAsync<bool>("canGoBack");
}
catch
{
errorMessage = "Navigazione indietro non disponibile";
}
}
private async Task NavigateForward()
{
try
{
await JSRuntime.InvokeVoidAsync("navigateForward");
canGoForward = await JSRuntime.InvokeAsync<bool>("canGoForward");
}
catch
{
errorMessage = "Navigazione avanti non disponibile";
}
}
private async Task RefreshPage()
{
try
{
await JSRuntime.InvokeVoidAsync("refreshFrame");
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Errore durante il refresh: {ex.Message}";
}
}
private async Task ExtractCookie()
{
try
{
// Tenta di estrarre il cookie tramite JavaScript
extractedCookie = await JSRuntime.InvokeAsync<string>("extractCookie", "bidoo.com");
if (string.IsNullOrEmpty(extractedCookie))
{
errorMessage = "Impossibile estrarre il cookie automaticamente. Le limitazioni CORS bloccano l'accesso. Usa il metodo manuale F12.";
}
else
{
// Copia automaticamente negli appunti
await CopyCookie();
errorMessage = null;
}
}
catch (Exception ex)
{
errorMessage = $"Errore estrazione cookie: {ex.Message}. Usa il metodo manuale con F12.";
extractedCookie = null;
}
}
private async Task CopyCookie()
{
if (!string.IsNullOrEmpty(extractedCookie))
{
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", extractedCookie);
await JSRuntime.InvokeVoidAsync("alert", "? Cookie copiato negli appunti!");
}
}
}

View File

@@ -1,406 +1,53 @@
@page "/freebids"
@inject AuctionMonitor AuctionMonitor
@inject IJSRuntime JSRuntime
<PageTitle>Puntate Gratuite - AutoBidder</PageTitle>
<div class="freebids-container animate-fade-in p-4">
<div class="d-flex align-items-center justify-content-between mb-4 animate-fade-in-down">
<div class="d-flex align-items-center">
<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>
<div class="d-flex gap-2">
<button class="btn btn-primary hover-lift" @onclick="RefreshData">
<i class="bi bi-arrow-clockwise"></i> Aggiorna
</button>
<button class="btn btn-success hover-lift" @onclick="ShowInfoModal">
<i class="bi bi-info-circle"></i> Come Funziona
</button>
</div>
<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>
<!-- Alert Feature Not Implemented -->
<div class="alert alert-info border-0 shadow-sm animate-scale-in mb-4">
<div class="d-flex align-items-center">
<i class="bi bi-clock-history me-3" style="font-size: 2.5rem;"></i>
<!-- Feature Under Development Notice -->
<div class="alert alert-info border-0 shadow-sm animate-scale-in">
<div class="d-flex align-items-start">
<i class="bi bi-tools me-3 mt-1" style="font-size: 3rem;"></i>
<div>
<h5 class="mb-2"><strong>?? Funzionalità in Sviluppo</strong></h5>
<p class="mb-2">
Il sistema di raccolta automatica delle puntate gratuite è attualmente in fase di sviluppo.
<h4 class="mb-3"><strong>Funzionalita in Sviluppo</strong></h4>
<p class="mb-3">
Il sistema di gestione delle puntate gratuite e attualmente in fase di sviluppo e sara disponibile in una prossima versione.
</p>
<p class="mb-0">
<strong>Prossimamente:</strong> Rilevamento automatico delle aste con puntate gratuite,
partecipazione automatica e statistiche dettagliate.
</p>
</div>
</div>
</div>
<!-- Stats Cards -->
<div class="row g-3 mb-4 animate-fade-in-up delay-100">
<div class="col-md-3">
<div class="card border-0 shadow-hover text-center">
<div class="card-body">
<i class="bi bi-gift text-primary" style="font-size: 2.5rem;"></i>
<h4 class="mt-3 mb-1 fw-bold">@totalFreeBids</h4>
<p class="text-muted mb-0">Puntate Gratuite Oggi</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-hover text-center">
<div class="card-body">
<i class="bi bi-check-circle text-success" style="font-size: 2.5rem;"></i>
<h4 class="mt-3 mb-1 fw-bold">@usedFreeBids</h4>
<p class="text-muted mb-0">Puntate Utilizzate</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-hover text-center">
<div class="card-body">
<i class="bi bi-clock text-warning" style="font-size: 2.5rem;"></i>
<h4 class="mt-3 mb-1 fw-bold">@pendingFreeBids</h4>
<p class="text-muted mb-0">In Attesa</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-hover text-center">
<div class="card-body">
<i class="bi bi-trophy text-info" style="font-size: 2.5rem;"></i>
<h4 class="mt-3 mb-1 fw-bold">@totalWins</h4>
<p class="text-muted mb-0">Aste Vinte</p>
</div>
</div>
</div>
</div>
<!-- Free Bids Table -->
<div class="card shadow-hover animate-fade-in-up delay-200">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-list-ul"></i> Aste con Puntate Gratuite Disponibili</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th><i class="bi bi-tag"></i> Prodotto</th>
<th><i class="bi bi-gift"></i> Puntate Gratuite</th>
<th><i class="bi bi-clock"></i> Scadenza</th>
<th><i class="bi bi-currency-euro"></i> Prezzo Attuale</th>
<th><i class="bi bi-speedometer2"></i> Stato</th>
<th><i class="bi bi-gear"></i> Azioni</th>
</tr>
</thead>
<tbody>
@if (freeBidsAuctions.Count == 0)
{
<tr>
<td colspan="6" class="text-center py-5">
<div class="text-muted">
<i class="bi bi-inbox" style="font-size: 3rem; display: block; margin-bottom: 1rem;"></i>
<h5>Nessuna asta con puntate gratuite disponibile</h5>
<p class="mb-0">Le aste appariranno automaticamente quando disponibili</p>
</div>
</td>
</tr>
}
else
{
@foreach (var auction in freeBidsAuctions)
{
<tr class="transition-all">
<td class="fw-semibold">@auction.ProductName</td>
<td>
<span class="badge bg-warning text-dark">
<i class="bi bi-gift-fill"></i> @auction.FreeBidsAvailable
</span>
</td>
<td>@auction.ExpiryTime.ToString("dd/MM/yyyy HH:mm")</td>
<td class="fw-bold text-success">€@auction.CurrentPrice.ToString("F2")</td>
<td>
<span class="badge @GetStatusBadgeClass(auction.Status)">
@auction.Status
</span>
</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-primary hover-scale" @onclick="() => UseFreeBids(auction)"
disabled="@(auction.Status != "Disponibile")">
<i class="bi bi-play-fill"></i> Usa
</button>
<button class="btn btn-info hover-scale" @onclick="() => ViewDetails(auction)">
<i class="bi bi-eye-fill"></i> Dettagli
</button>
</div>
</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</div>
<!-- Configuration Card -->
<div class="card shadow-hover mt-4 animate-fade-in-up delay-300">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-sliders"></i> Configurazione Puntate Gratuite</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="autoCollect" @bind="autoCollectEnabled" />
<label class="form-check-label" for="autoCollect">
<i class="bi bi-magic"></i> Raccolta automatica puntate gratuite
</label>
</div>
<small class="form-text text-muted">
Rileva e raccogli automaticamente le puntate gratuite disponibili
</small>
</div>
<div class="col-md-6 mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="autoUse" @bind="autoUseEnabled" />
<label class="form-check-label" for="autoUse">
<i class="bi bi-lightning-charge"></i> Utilizzo automatico puntate gratuite
</label>
</div>
<small class="form-text text-muted">
Usa automaticamente le puntate gratuite sulle aste selezionate
</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
<i class="bi bi-currency-euro"></i> Prezzo massimo per auto-uso:
</label>
<input type="number" step="0.01" class="form-control transition-colors" @bind="maxPriceAutoUse" />
<small class="form-text text-muted">
Usa puntate gratuite solo su aste sotto questo prezzo
</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
<i class="bi bi-hourglass-split"></i> Tempo minimo rimanente (minuti):
</label>
<input type="number" class="form-control transition-colors" @bind="minTimeRemaining" />
<small class="form-text text-muted">
Tempo minimo prima della scadenza per usare puntate gratuite
</small>
</div>
</div>
<button class="btn btn-success hover-lift" @onclick="SaveConfiguration" disabled>
<i class="bi bi-check-lg"></i> Salva Configurazione
</button>
<small class="text-muted ms-2">
<i class="bi bi-info-circle"></i> Disponibile nella prossima versione
</small>
</div>
</div>
<!-- How It Works Guide -->
<div class="card border-info mt-4 animate-fade-in-up delay-400">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-question-circle"></i> Come Funzioneranno le Puntate Gratuite</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="fw-bold text-primary"><i class="bi bi-1-circle-fill"></i> Rilevamento Automatico</h6>
<p>
AutoBidder scansionerà continuamente Bidoo.com per identificare le aste che offrono puntate gratuite.
</p>
<h6 class="fw-bold text-primary mt-3"><i class="bi bi-2-circle-fill"></i> Raccolta Puntate</h6>
<p>
Le puntate gratuite verranno raccolte automaticamente non appena disponibili, prima che scadano.
</p>
<h6 class="fw-bold text-primary mt-3"><i class="bi bi-3-circle-fill"></i> Utilizzo Strategico</h6>
<p>
Le puntate verranno utilizzate secondo le tue preferenze configurate (prezzo max, tempo rimanente, ecc.).
</p>
</div>
<div class="col-md-6">
<h6 class="fw-bold text-success"><i class="bi bi-check-circle-fill"></i> Vantaggi</h6>
<ul>
<li>? Zero costo per le puntate gratuite</li>
<li>? Maggiori opportunità di vincita</li>
<li>? Nessun rischio economico</li>
<li>? Gestione completamente automatica</li>
</ul>
<h6 class="fw-bold text-warning mt-3"><i class="bi bi-exclamation-triangle-fill"></i> Note Importanti</h6>
<ul>
<li>?? Le puntate gratuite hanno scadenza limitata</li>
<li>?? Disponibilità limitata per prodotto/utente</li>
<li>?? Vincere comunque richiede pagamento prodotto</li>
</ul>
</div>
</div>
<div class="alert alert-secondary border-0 mt-3 mb-0">
<div class="d-flex align-items-center">
<i class="bi bi-lightbulb-fill me-3" style="font-size: 1.5rem;"></i>
<div>
<strong>Suggerimento:</strong> Combina le puntate gratuite con la strategia di bidding normale
per massimizzare le tue possibilità di vincita minimizzando i costi.
</div>
<hr class="my-3" />
<h5 class="mb-2"><i class="bi bi-lightbulb-fill text-warning"></i> Funzionalita Previste:</h5>
<ul class="mb-3">
<li><strong>Rilevamento Automatico:</strong> Scansione continua delle aste con puntate gratuite disponibili</li>
<li><strong>Raccolta Automatica:</strong> Acquisizione automatica delle puntate gratuite prima della scadenza</li>
<li><strong>Utilizzo Strategico:</strong> Uso intelligente delle puntate secondo criteri configurabili</li>
<li><strong>Statistiche Dettagliate:</strong> Tracciamento completo di utilizzo, vincite e risparmi</li>
<li><strong>Notifiche:</strong> Avvisi in tempo reale per nuove opportunita</li>
</ul>
<div class="alert alert-secondary border-0 mb-0">
<i class="bi bi-info-circle-fill me-2"></i>
<strong>Nota:</strong> Le puntate gratuite sono offerte speciali di Bidoo che permettono di partecipare
ad alcune aste senza utilizzare i propri crediti. Questa funzionalita automatizzera completamente
il processo di raccolta e utilizzo.
</div>
</div>
</div>
</div>
</div>
<!-- Modal Info -->
@if (showInfoModal)
{
<div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content animate-scale-in">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-info-circle-fill"></i> Informazioni Puntate Gratuite</h5>
<button type="button" class="btn-close" @onclick="CloseInfoModal"></button>
</div>
<div class="modal-body">
<h6 class="fw-bold text-primary">?? Cosa sono le Puntate Gratuite?</h6>
<p>
Le puntate gratuite sono offerte speciali di Bidoo che permettono di partecipare a determinate aste
senza utilizzare i tuoi crediti di puntata.
</p>
<h6 class="fw-bold text-primary mt-3">?? Come Funziona AutoBidder (Prossimamente)</h6>
<ol>
<li><strong>Rilevamento:</strong> Monitora continuamente Bidoo per nuove offerte</li>
<li><strong>Raccolta:</strong> Raccoglie automaticamente le puntate gratuite disponibili</li>
<li><strong>Utilizzo:</strong> Le usa strategicamente secondo le tue impostazioni</li>
<li><strong>Report:</strong> Tiene traccia di utilizzo e risultati</li>
</ol>
<h6 class="fw-bold text-success mt-3">? Vantaggi</h6>
<ul>
<li>Partecipa ad aste senza costi</li>
<li>Testa prodotti prima di investire puntate normali</li>
<li>Aumenta le probabilità di vincita generale</li>
</ul>
<h6 class="fw-bold text-warning mt-3">?? Limitazioni</h6>
<ul>
<li>Scadenza temporale (di solito 24-48 ore)</li>
<li>Disponibilità limitata per account</li>
<li>Solo su prodotti selezionati da Bidoo</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary hover-lift" @onclick="CloseInfoModal">
<i class="bi bi-check-lg"></i> Ho Capito
</button>
</div>
</div>
</div>
</div>
}
<style>
.freebids-container {
max-width: 1600px;
max-width: 1200px;
margin: 0 auto;
}
.alert ul {
padding-left: 1.5rem;
}
.alert ul li {
margin-bottom: 0.5rem;
}
</style>
@code {
// Stats
private int totalFreeBids = 0;
private int usedFreeBids = 0;
private int pendingFreeBids = 0;
private int totalWins = 0;
// Configuration
private bool autoCollectEnabled = false;
private bool autoUseEnabled = false;
private decimal maxPriceAutoUse = 5.0m;
private int minTimeRemaining = 10;
// Data
private List<FreeBidAuction> freeBidsAuctions = new();
private bool showInfoModal = false;
protected override void OnInitialized()
{
LoadMockData();
}
private void LoadMockData()
{
// Mock data for demonstration
totalFreeBids = 0;
usedFreeBids = 0;
pendingFreeBids = 0;
totalWins = 0;
// Empty list - funzionalità non ancora implementata
freeBidsAuctions = new List<FreeBidAuction>();
}
private async Task RefreshData()
{
LoadMockData();
await JSRuntime.InvokeVoidAsync("alert", "?? Funzionalità in sviluppo. I dati verranno aggiornati nella prossima versione.");
}
private void ShowInfoModal()
{
showInfoModal = true;
}
private void CloseInfoModal()
{
showInfoModal = false;
}
private async Task UseFreeBids(FreeBidAuction auction)
{
await JSRuntime.InvokeVoidAsync("alert", $"?? Funzionalità in sviluppo.\n\nSarà possibile utilizzare le puntate gratuite su: {auction.ProductName}");
}
private async Task ViewDetails(FreeBidAuction auction)
{
await JSRuntime.InvokeVoidAsync("alert", $"?? Dettagli Asta\n\nProdotto: {auction.ProductName}\nPuntate Gratuite: {auction.FreeBidsAvailable}\nPrezzo: €{auction.CurrentPrice:F2}\nScadenza: {auction.ExpiryTime:dd/MM/yyyy HH:mm}");
}
private async Task SaveConfiguration()
{
await JSRuntime.InvokeVoidAsync("alert", "?? Configurazione sarà disponibile nella prossima versione.");
}
private string GetStatusBadgeClass(string status)
{
return status switch
{
"Disponibile" => "bg-success",
"In Uso" => "bg-warning text-dark",
"Scaduta" => "bg-danger",
"Completata" => "bg-info",
_ => "bg-secondary"
};
}
// Model
private class FreeBidAuction
{
public string ProductName { get; set; } = "";
public int FreeBidsAvailable { get; set; }
public DateTime ExpiryTime { get; set; }
public decimal CurrentPrice { get; set; }
public string Status { get; set; } = "Disponibile";
}
}

View File

@@ -0,0 +1,35 @@
@page "/health"
@inject DatabaseService DatabaseService
@inject AuctionMonitor AuctionMonitor
@code {
protected override async Task OnInitializedAsync()
{
try
{
// Verifica database
var dbHealthy = await DatabaseService.CheckDatabaseHealthAsync();
// Verifica servizi
var auctionsCount = AuctionMonitor.GetAuctions().Count;
if (dbHealthy)
{
// Ritorna 200 OK
await Task.CompletedTask;
}
else
{
// Ritorna 500 Internal Server Error
throw new Exception("Database health check failed");
}
}
catch
{
// Healthcheck fallito
throw;
}
}
}
OK

View File

@@ -48,8 +48,10 @@
<th><i class="bi bi-currency-euro"></i> Prezzo</th>
<th><i class="bi bi-clock"></i> Timer</th>
<th><i class="bi bi-person"></i> Ultimo</th>
<th><i class="bi bi-arrow-repeat"></i> Reset</th>
<th><i class="bi bi-hand-index"></i> Click</th>
<th><i class="bi bi-calculator"></i> Totale</th>
<th><i class="bi bi-piggy-bank"></i> Risparmio</th>
<th><i class="bi bi-check-circle"></i> OK?</th>
<th><i class="bi bi-gear"></i> Azioni</th>
</tr>
</thead>
@@ -68,8 +70,10 @@
<td class="@GetPriceClass(auction)">@GetPriceDisplay(auction)</td>
<td>@GetTimerDisplay(auction)</td>
<td>@GetLastBidder(auction)</td>
<td><span class="badge bg-secondary">@auction.ResetCount</span></td>
<td><span class="badge bg-info">@GetMyBidsCount(auction)</span></td>
<td class="@GetPriceClass(auction)">@GetTotalCostDisplay(auction)</td>
<td class="@GetSavingsClass(auction)">@GetSavingsDisplay(auction)</td>
<td><span class="@GetIsWorthItClass(auction)">@GetIsWorthItIcon(auction)</span></td>
<td>
<div class="btn-group btn-group-sm" @onclick:stopPropagation="true">
@if (auction.IsActive && !auction.IsPaused)
@@ -103,23 +107,42 @@
}
</div>
<!-- LOG GLOBALE - ALTO DESTRA -->
<div class="global-log animate-fade-in-up delay-200">
<div class="d-flex justify-content-between align-items-center">
<h4 class="mb-0"><i class="bi bi-terminal"></i> Log Globale</h4>
<button class="btn btn-sm btn-secondary" @onclick="ClearGlobalLog">
<i class="bi bi-trash"></i>
</button>
</div>
<div class="log-box">
@if (globalLog.Count == 0)
{
<div class="text-muted"><i class="bi bi-inbox"></i> Nessun log ancora...</div>
}
else
{
@foreach (var logEntry in globalLog.TakeLast(100))
{
<div class="@GetLogEntryClass(logEntry)">@logEntry</div>
}
}
</div>
</div>
<!-- DETTAGLI ASTA - BASSO DESTRA -->
@if (selectedAuction != null)
{
<div class="auction-details animate-fade-in-right delay-200 shadow-hover">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="mb-0"><i class="bi bi-info-circle-fill text-primary"></i> @selectedAuction.Name</h3>
<span class="badge @GetStatusBadgeClass(selectedAuction) badge-glow fs-6">
@GetStatusIcon(selectedAuction) @GetStatusText(selectedAuction)
</span>
</div>
<div class="auction-details animate-fade-in-right delay-300 shadow-hover">
<h3><i class="bi bi-info-circle-fill"></i> @selectedAuction.Name</h3>
<p><small class="text-muted"><i class="bi bi-hash"></i> ID: @selectedAuction.AuctionId</small></p>
<div class="auction-info animate-scale-in">
<div class="auction-info">
<div class="info-group">
<label><i class="bi bi-link-45deg"></i> URL:</label>
<div class="input-group">
<input type="text" class="form-control" value="@selectedAuction.OriginalUrl" readonly />
<button class="btn btn-outline-secondary hover-glow" @onclick="() => CopyToClipboard(selectedAuction.OriginalUrl)" title="Copia">
<button class="btn btn-outline-secondary" @onclick="() => CopyToClipboard(selectedAuction.OriginalUrl)" title="Copia">
<i class="bi bi-clipboard"></i>
</button>
</div>
@@ -127,160 +150,150 @@
<div class="row">
<div class="col-md-6 info-group">
<label><i class="bi bi-speedometer2"></i> Anticipo Puntata (ms):</label>
<input type="number" class="form-control transition-colors" @bind="selectedAuction.BidBeforeDeadlineMs" @bind:after="SaveAuctions" />
<label><i class="bi bi-speedometer2"></i> Anticipo (ms):</label>
<input type="number" class="form-control" @bind="selectedAuction.BidBeforeDeadlineMs" @bind:after="SaveAuctions" />
</div>
<div class="col-md-6 info-group">
<label><i class="bi bi-hand-index-thumb"></i> Max Click:</label>
<input type="number" class="form-control transition-colors" @bind="selectedAuction.MaxClicks" @bind:after="SaveAuctions" />
<input type="number" class="form-control" @bind="selectedAuction.MaxClicks" @bind:after="SaveAuctions" />
</div>
</div>
<div class="row">
<div class="col-md-6 info-group">
<label><i class="bi bi-currency-euro"></i> Min Prezzo (€):</label>
<input type="number" step="0.01" class="form-control transition-colors" @bind="selectedAuction.MinPrice" @bind:after="SaveAuctions" />
<label><i class="bi bi-currency-euro"></i> Min :</label>
<input type="number" step="0.01" class="form-control" @bind="selectedAuction.MinPrice" @bind:after="SaveAuctions" />
</div>
<div class="col-md-6 info-group">
<label><i class="bi bi-currency-euro"></i> Max Prezzo (€):</label>
<input type="number" step="0.01" class="form-control transition-colors" @bind="selectedAuction.MaxPrice" @bind:after="SaveAuctions" />
<label><i class="bi bi-currency-euro"></i> Max :</label>
<input type="number" step="0.01" class="form-control" @bind="selectedAuction.MaxPrice" @bind:after="SaveAuctions" />
</div>
</div>
<div class="form-check mt-3">
<div class="row">
<div class="col-md-6 info-group">
<label><i class="bi bi-arrow-repeat"></i> Min Reset:</label>
<input type="number" class="form-control" @bind="selectedAuction.MinResets" @bind:after="SaveAuctions" />
</div>
<div class="col-md-6 info-group">
<label><i class="bi bi-arrow-repeat"></i> Max Reset:</label>
<input type="number" class="form-control" @bind="selectedAuction.MaxResets" @bind:after="SaveAuctions" />
</div>
</div>
<div class="form-check mt-2">
<input type="checkbox" class="form-check-input" id="checkOpen" @bind="selectedAuction.CheckAuctionOpenBeforeBid" @bind:after="SaveAuctions" />
<label class="form-check-label" for="checkOpen">
<i class="bi bi-shield-check"></i> Verifica asta aperta prima di puntare
Verifica asta aperta
</label>
</div>
</div>
<div class="auction-log mt-4">
<h4><i class="bi bi-journal-text"></i> Log Asta</h4>
<div class="log-box">
@if (GetAuctionLog(selectedAuction).Any())
{
@foreach (var logEntry in GetAuctionLog(selectedAuction))
{
<div class="log-entry-new">@logEntry</div>
}
}
else
{
<div class="text-muted"><i class="bi bi-inbox"></i> Nessun log disponibile</div>
}
</div>
</div>
<div class="bidders-stats mt-4">
<h4><i class="bi bi-people-fill"></i> Partecipanti (@selectedAuction.BidderStats.Count)</h4>
@if (selectedAuction.BidderStats.Count == 0)
@* ?? NUOVO: Sezione Valore Prodotto *@
@if (selectedAuction.CalculatedValue != null)
{
<p class="text-muted"><i class="bi bi-person-x"></i> Nessun partecipante ancora</p>
}
else
{
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th><i class="bi bi-person"></i> Utente</th>
<th><i class="bi bi-hand-index"></i> Puntate</th>
<th><i class="bi bi-clock-history"></i> Ultima</th>
</tr>
</thead>
<tbody>
@foreach (var bidder in selectedAuction.BidderStats.Values.OrderByDescending(b => b.BidCount))
{
<tr class="transition-all">
<td><strong>@bidder.Username</strong></td>
<td><span class="badge bg-primary">@bidder.BidCount</span></td>
<td>@bidder.LastBidTimeDisplay</td>
</tr>
}
</tbody>
</table>
<hr class="my-3" />
<h5 class="mb-3"><i class="bi bi-calculator-fill"></i> Valore Prodotto</h5>
<div class="row g-2">
<div class="col-md-6">
<div class="card border-0 bg-light">
<div class="card-body p-2 text-center">
<small class="text-muted d-block">Prezzo Compra Subito</small>
<strong class="d-block">@GetBuyNowPriceDisplay(selectedAuction)</strong>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-0 bg-light">
<div class="card-body p-2 text-center">
<small class="text-muted d-block">Costo Totale</small>
<strong class="d-block">@GetTotalCostDisplay(selectedAuction)</strong>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-0 bg-light">
<div class="card-body p-2 text-center">
<small class="text-muted d-block">Risparmio</small>
<strong class="d-block @GetSavingsClass(selectedAuction)">@GetSavingsDisplay(selectedAuction)</strong>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-0 bg-light">
<div class="card-body p-2 text-center">
<small class="text-muted d-block">Conveniente?</small>
<span class="@GetIsWorthItClass(selectedAuction) d-inline-block mt-1">
@GetIsWorthItIcon(selectedAuction)
</span>
</div>
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(selectedAuction.CalculatedValue.Summary))
{
<div class="alert alert-info mt-2 mb-0 p-2">
<small><i class="bi bi-info-circle"></i> @selectedAuction.CalculatedValue.Summary</small>
</div>
}
}
</div>
</div>
}
else
{
<div class="auction-details animate-fade-in shadow-hover">
<div class="alert alert-secondary text-center animate-pulse" style="margin-top: 50px;">
<i class="bi bi-arrow-left animate-bounce" style="font-size: 3rem; display: block; margin-bottom: 1rem;"></i>
<h4>Seleziona un'asta</h4>
<p class="mb-0">Clicca su un'asta dalla lista per visualizzare i dettagli</p>
<div class="auction-details animate-fade-in">
<div class="alert alert-secondary text-center">
<i class="bi bi-arrow-left" style="font-size: 2rem; display: block; margin-bottom: 0.5rem;"></i>
<p class="mb-0">Seleziona un'asta per i dettagli</p>
</div>
</div>
}
</div>
<div class="global-log mt-3 animate-fade-in-up delay-300">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0"><i class="bi bi-terminal"></i> Log Globale</h4>
<button class="btn btn-sm btn-secondary hover-lift" @onclick="ClearGlobalLog">
<i class="bi bi-trash"></i> Pulisci
</button>
</div>
<div class="log-box">
@if (globalLog.Count == 0)
{
<div class="text-muted"><i class="bi bi-inbox"></i> Nessun log ancora...</div>
}
else
{
@foreach (var logEntry in globalLog.TakeLast(100))
{
<div class="@GetLogEntryClass(logEntry)">@logEntry</div>
}
}
</div>
</div>
</div>
<!-- Modal Aggiungi Asta -->
@if (showAddDialog)
{
<div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content animate-scale-in">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-plus-circle"></i> Aggiungi Nuova Asta</h5>
<button type="button" class="btn-close" @onclick="CloseAddDialog"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-bold"><i class="bi bi-link-45deg"></i> URL Asta:</label>
<input type="text" class="form-control transition-colors @(addDialogError != null ? "is-invalid" : "")"
@bind="addDialogUrl"
placeholder="https://it.bidoo.com/asta/..." />
@if (addDialogError != null)
{
<div class="invalid-feedback d-block animate-shake">
<i class="bi bi-exclamation-triangle"></i> @addDialogError
</div>
}
<small class="form-text text-muted">
<i class="bi bi-info-circle"></i> Inserisci l'URL completo dell'asta da Bidoo.com
</small>
<!-- Modal Aggiungi Asta -->
@if (showAddDialog)
{
<div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content animate-scale-in">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-plus-circle"></i> Aggiungi Nuova Asta</h5>
<button type="button" class="btn-close" @onclick="CloseAddDialog"></button>
</div>
<div class="mb-3">
<label class="form-label fw-bold"><i class="bi bi-tag"></i> Nome Asta (opzionale):</label>
<input type="text" class="form-control transition-colors" @bind="addDialogName" placeholder="Es: iPhone 15 Pro" />
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-bold"><i class="bi bi-link-45deg"></i> URL Asta:</label>
<input type="text" class="form-control transition-colors @(addDialogError != null ? "is-invalid" : "")"
@bind="addDialogUrl"
placeholder="https://it.bidoo.com/asta/..." />
@if (addDialogError != null)
{
<div class="invalid-feedback d-block animate-shake">
<i class="bi bi-exclamation-triangle"></i> @addDialogError
</div>
}
<small class="form-text text-muted">
<i class="bi bi-info-circle"></i> Inserisci l'URL completo dell'asta da Bidoo.com
</small>
</div>
<div class="mb-3">
<label class="form-label fw-bold"><i class="bi bi-tag"></i> Nome Asta (opzionale):</label>
<input type="text" class="form-control transition-colors" @bind="addDialogName" placeholder="Es: iPhone 15 Pro" />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary hover-lift" @onclick="CloseAddDialog">
<i class="bi bi-x-circle"></i> Annulla
</button>
<button type="button" class="btn btn-primary hover-lift" @onclick="AddAuction" disabled="@string.IsNullOrWhiteSpace(addDialogUrl)">
<i class="bi bi-plus-lg"></i> Aggiungi
</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary hover-lift" @onclick="CloseAddDialog">
<i class="bi bi-x-circle"></i> Annulla
</button>
<button type="button" class="btn btn-primary hover-lift" @onclick="AddAuction" disabled="@string.IsNullOrWhiteSpace(addDialogUrl)">
<i class="bi bi-plus-lg"></i> Aggiungi
</button>
</div>
</div>
</div>
</div>
}
}
</div>

View File

@@ -35,7 +35,10 @@ namespace AutoBidder.Pages
await InvokeAsync(StateHasChanged);
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
AddLog("Applicazione avviata");
AddLog("? Applicazione avviata");
// Applica logica auto-start in base alle impostazioni
ApplyAutoStartLogic();
}
private void LoadAuctionsFromDisk()
@@ -50,7 +53,78 @@ namespace AutoBidder.Pages
if (loadedAuctions.Count > 0)
{
AddLog($"Caricate {loadedAuctions.Count} aste salvate");
AddLog($"?? Caricate {loadedAuctions.Count} aste salvate");
}
}
private void ApplyAutoStartLogic()
{
var settings = AutoBidder.Utilities.SettingsManager.Load();
if (settings.RememberAuctionStates)
{
// Modalità "Ricorda Stato": mantiene lo stato salvato di ogni asta
var activeCount = auctions.Count(a => a.IsActive && !a.IsPaused);
if (activeCount > 0)
{
AuctionMonitor.Start();
isMonitoringActive = true;
AddLog($"?? [REMEMBER STATE] Ripristinato stato salvato: {activeCount} aste attive");
}
else
{
AddLog("?? [REMEMBER STATE] Nessuna asta attiva salvata");
}
}
else
{
// Modalità "Default": applica DefaultStartAuctionsOnLoad a tutte le aste
switch (settings.DefaultStartAuctionsOnLoad)
{
case "Active":
// Avvia tutte le aste
foreach (var auction in auctions)
{
auction.IsActive = true;
auction.IsPaused = false;
}
if (auctions.Count > 0)
{
AuctionMonitor.Start();
isMonitoringActive = true;
SaveAuctions();
AddLog($"?? [AUTO-START] Avviate automaticamente {auctions.Count} aste");
}
break;
case "Paused":
// Mette in pausa tutte le aste
foreach (var auction in auctions)
{
auction.IsActive = true;
auction.IsPaused = true;
}
if (auctions.Count > 0)
{
AuctionMonitor.Start();
isMonitoringActive = true;
SaveAuctions();
AddLog($"?? [AUTO-START] Aste in pausa: {auctions.Count}");
}
break;
case "Stopped":
default:
// Ferma tutte le aste (default)
foreach (var auction in auctions)
{
auction.IsActive = false;
auction.IsPaused = false;
}
SaveAuctions();
AddLog($"?? [AUTO-START] Aste fermate all'avvio: {auctions.Count}");
break;
}
}
}
@@ -77,6 +151,9 @@ namespace AutoBidder.Pages
var auction = auctions.FirstOrDefault(a => a.AuctionId == state.AuctionId);
if (auction != null)
{
// Salva l'ultimo stato ricevuto
auction.LastState = state;
InvokeAsync(StateHasChanged);
}
}
@@ -129,10 +206,12 @@ namespace AutoBidder.Pages
auction.IsActive = true;
auction.IsPaused = false;
// Auto-start monitor se non attivo
if (!isMonitoringActive)
{
AuctionMonitor.Start();
isMonitoringActive = true;
AddLog("[AUTO-START] Monitoraggio avviato");
}
SaveAuctions();
@@ -159,6 +238,14 @@ namespace AutoBidder.Pages
auction.IsPaused = false;
SaveAuctions();
AddLog($"?? Fermata asta: {auction.Name}");
// Auto-stop monitor se nessuna asta è attiva
if (!auctions.Any(a => a.IsActive))
{
AuctionMonitor.Stop();
isMonitoringActive = false;
AddLog("[AUTO-STOP] Monitoraggio fermato");
}
}
// Dialog Aggiungi Asta
@@ -204,31 +291,75 @@ namespace AutoBidder.Pages
// Carica impostazioni default
var settings = AutoBidder.Utilities.SettingsManager.Load();
// Crea nuova asta
// Determina stato iniziale in base a DefaultNewAuctionState
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;
}
// Crea nuova asta con nome temporaneo
var tempName = string.IsNullOrWhiteSpace(addDialogName) ? $"Caricamento... (ID: {auctionId})" : addDialogName;
var newAuction = new AuctionInfo
{
AuctionId = auctionId,
Name = string.IsNullOrWhiteSpace(addDialogName) ? $"Asta {auctionId}" : addDialogName,
Name = tempName,
OriginalUrl = addDialogUrl,
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs,
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
MinPrice = settings.DefaultMinPrice,
MaxPrice = settings.DefaultMaxPrice,
MaxClicks = settings.DefaultMaxClicks,
IsActive = false,
IsPaused = false
MinResets = settings.DefaultMinResets,
MaxResets = settings.DefaultMaxResets,
IsActive = isActive,
IsPaused = isPaused
};
auctions.Add(newAuction);
AuctionMonitor.AddAuction(newAuction);
SaveAuctions();
AddLog($"? Aggiunta asta: {newAuction.Name} (ID: {auctionId})");
// Log stato iniziale
string stateLabel = settings.DefaultNewAuctionState switch
{
"Active" => "?? Attiva",
"Paused" => "?? In Pausa",
_ => "?? Fermata"
};
AddLog($"? Aggiunta asta: {newAuction.Name} (ID: {auctionId}) - Stato: {stateLabel}");
// Auto-start monitor se necessario
if (isActive && !isMonitoringActive)
{
AuctionMonitor.Start();
isMonitoringActive = true;
AddLog("[AUTO-START] Monitoraggio avviato per nuova asta attiva");
}
CloseAddDialog();
selectedAuction = newAuction;
// ?? TODO: Implementare caricamento nome e info prodotto in background
// Richiede aggiunta metodo GetAuctionHtmlAsync a BidooApiClient
}
private string ExtractAuctionId(string url)
{
try
@@ -309,36 +440,194 @@ namespace AutoBidder.Pages
return "status-active";
}
private string GetPriceDisplay(AuctionInfo auction)
private string GetPriceDisplay(AuctionInfo? auction)
{
if (auction.CalculatedValue?.CurrentPrice > 0)
return $"€{auction.CalculatedValue.CurrentPrice:F2}";
try
{
if (auction == null) return "-";
// Prova a leggere da LastState prima
if (auction.LastState != null && auction.LastState.Price > 0)
return $"€{auction.LastState.Price:F2}";
// Fallback a CalculatedValue
if (auction.CalculatedValue?.CurrentPrice > 0)
return $"€{auction.CalculatedValue.CurrentPrice:F2}";
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] GetPriceDisplay: {ex.Message}");
}
return "-";
}
private string GetPriceClass(AuctionInfo auction)
private string GetPriceClass(AuctionInfo? auction)
{
if (auction.CalculatedValue?.CurrentPrice > 0)
return "fw-bold text-success";
try
{
if (auction == null) return "text-muted";
double price = auction.LastState?.Price ?? auction.CalculatedValue?.CurrentPrice ?? 0;
if (price > 0)
return "fw-bold text-success";
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] GetPriceClass: {ex.Message}");
}
return "text-muted";
}
private string GetTimerDisplay(AuctionInfo auction)
private string GetTimerDisplay(AuctionInfo? auction)
{
// TODO: Get from latest state
try
{
if (auction == null) return "-";
if (auction.LastState == null || auction.LastState.Timer <= 0)
return "-";
var ts = TimeSpan.FromSeconds(auction.LastState.Timer);
if (ts.TotalHours >= 1)
return $"{(int)ts.TotalHours}h {ts.Minutes}m";
if (ts.TotalMinutes >= 1)
return $"{ts.Minutes}m {ts.Seconds}s";
return $"{ts.Seconds}s";
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] GetTimerDisplay: {ex.Message}");
}
return "-";
}
private string GetLastBidder(AuctionInfo auction)
private string GetLastBidder(AuctionInfo? auction)
{
return auction.RecentBids.FirstOrDefault()?.Username ?? "-";
try
{
if (auction == null) return "-";
return auction.RecentBids?.FirstOrDefault()?.Username ?? "-";
}
catch
{
return "-";
}
}
private int GetMyBidsCount(AuctionInfo auction)
private int GetMyBidsCount(AuctionInfo? auction)
{
return auction.BidHistory.Count(b => b.EventType == BidEventType.MyBid);
try
{
if (auction == null) return 0;
return auction.BidHistory?.Count(b => b?.EventType == BidEventType.MyBid) ?? 0;
}
catch
{
return 0;
}
}
// ?? NUOVI METODI: Visualizzazione valori prodotto
private string GetTotalCostDisplay(AuctionInfo? auction)
{
try
{
if (auction?.CalculatedValue != null)
{
return $"€{auction.CalculatedValue.TotalCostIfWin:F2}";
}
}
catch { }
return "-";
}
private string GetSavingsDisplay(AuctionInfo? auction)
{
try
{
if (auction?.CalculatedValue?.Savings.HasValue == true)
{
var savings = auction.CalculatedValue.Savings.Value;
var percentage = auction.CalculatedValue.SavingsPercentage ?? 0;
if (savings > 0)
return $"+€{savings:F2} ({percentage:F0}%)";
else
return $"-€{Math.Abs(savings):F2} ({Math.Abs(percentage):F0}%)";
}
}
catch { }
return "-";
}
private string GetSavingsClass(AuctionInfo? auction)
{
try
{
if (auction?.CalculatedValue?.Savings.HasValue == true)
{
return auction.CalculatedValue.Savings.Value > 0
? "text-success fw-bold" // Verde per risparmio
: "text-danger fw-bold"; // Rosso per perdita
}
}
catch { }
return "text-muted";
}
private string GetBuyNowPriceDisplay(AuctionInfo? auction)
{
try
{
if (auction?.BuyNowPrice.HasValue == true)
{
return $"€{auction.BuyNowPrice.Value:F2}";
}
}
catch { }
return "-";
}
private string GetIsWorthItIcon(AuctionInfo? auction)
{
try
{
if (auction?.CalculatedValue != null)
{
return auction.CalculatedValue.IsWorthIt ? "?" : "?";
}
}
catch { }
return "-";
}
private string GetIsWorthItClass(AuctionInfo? auction)
{
try
{
if (auction?.CalculatedValue != null)
{
return auction.CalculatedValue.IsWorthIt
? "badge bg-success"
: "badge bg-danger";
}
}
catch { }
return "badge bg-secondary";
}
private IEnumerable<string> GetAuctionLog(AuctionInfo auction)
{
return auction.AuctionLog.TakeLast(50);
@@ -346,11 +635,18 @@ namespace AutoBidder.Pages
private string GetLogEntryClass(string logEntry)
{
if (logEntry.Contains("?") || logEntry.Contains("Errore") || logEntry.Contains("errore"))
return "log-entry-error";
if (logEntry.Contains("??") || logEntry.Contains("Warning") || logEntry.Contains("warning"))
return "log-entry-warning";
return "log-entry-new";
try
{
if (logEntry.Contains("[ERROR]") || logEntry.Contains("Errore") || logEntry.Contains("errore") || logEntry.Contains("FAIL"))
return "log-entry-error";
if (logEntry.Contains("[WARN]") || logEntry.Contains("Warning") || logEntry.Contains("warning"))
return "log-entry-warning";
if (logEntry.Contains("[OK]") || logEntry.Contains("SUCCESS") || logEntry.Contains("Vinta"))
return "log-entry-success";
}
catch { }
return "log-entry-info";
}
public void Dispose()

View File

@@ -2,6 +2,7 @@
@inject SessionService SessionService
@inject AuctionMonitor AuctionMonitor
@inject IJSRuntime JSRuntime
@implements IDisposable
<PageTitle>Impostazioni - AutoBidder</PageTitle>
@@ -11,6 +12,7 @@
<h2 class="mb-0 fw-bold">Impostazioni</h2>
</div>
<!-- SESSIONE BIDOO -->
<div class="card mb-4 shadow-hover animate-fade-in-up delay-100">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-wifi"></i> Sessione Bidoo</h5>
@@ -54,7 +56,7 @@
placeholder="Incolla qui il cookie di sessione da browser..."></textarea>
<small class="form-text text-muted">
<i class="bi bi-info-circle"></i> Apri gli strumenti sviluppatore del browser (F12), vai alla tab "Network",
visita bidoo.com, copia il valore del cookie "__stattrb" o simile.
visita bidoo.com, copia il valore del cookie.
</small>
</div>
@@ -72,7 +74,7 @@
<button class="btn btn-primary hover-lift" @onclick="Connect" disabled="@(string.IsNullOrEmpty(cookieInput) || isConnecting)">
@if (isConnecting)
{
<span class="spinner-border spinner-border-sm me-2 animate-spin"></span>
<span class="spinner-border spinner-border-sm me-2"></span>
<span>Connessione...</span>
}
else
@@ -85,6 +87,102 @@
</div>
</div>
<!-- COMPORTAMENTO AVVIO ASTE -->
<div class="card mb-4 shadow-hover animate-fade-in-up delay-150">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0"><i class="bi bi-power"></i> Comportamento Avvio Aste</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
<i class="bi bi-folder-open"></i> Stato Aste al Caricamento:
</label>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="loadState" id="loadRemember"
checked="@settings.RememberAuctionStates" @onclick="@(() => SetRememberState(true))" />
<label class="form-check-label" for="loadRemember">
<i class="bi bi-memory"></i> <strong>Ricorda Stato</strong> - Mantiene lo stato salvato
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="loadState" id="loadActive"
checked="@(!settings.RememberAuctionStates && settings.DefaultStartAuctionsOnLoad == "Active")"
@onclick="@(() => SetLoadState("Active"))" />
<label class="form-check-label" for="loadActive">
<i class="bi bi-play-circle-fill text-success"></i> <strong>Avvia Tutte</strong>
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="loadState" id="loadPaused"
checked="@(!settings.RememberAuctionStates && settings.DefaultStartAuctionsOnLoad == "Paused")"
@onclick="@(() => SetLoadState("Paused"))" />
<label class="form-check-label" for="loadPaused">
<i class="bi bi-pause-circle-fill text-warning"></i> <strong>In Pausa</strong>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="loadState" id="loadStopped"
checked="@(!settings.RememberAuctionStates && settings.DefaultStartAuctionsOnLoad == "Stopped")"
@onclick="@(() => SetLoadState("Stopped"))" />
<label class="form-check-label" for="loadStopped">
<i class="bi bi-stop-circle-fill text-danger"></i> <strong>Fermate</strong> (default)
</label>
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
<i class="bi bi-plus-circle"></i> Stato Nuove Aste:
</label>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="newState" id="newActive"
checked="@(settings.DefaultNewAuctionState == "Active")"
@onclick="@(() => SetNewAuctionState("Active"))" />
<label class="form-check-label" for="newActive">
<i class="bi bi-play-circle-fill text-success"></i> <strong>Attiva</strong>
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="newState" id="newPaused"
checked="@(settings.DefaultNewAuctionState == "Paused")"
@onclick="@(() => SetNewAuctionState("Paused"))" />
<label class="form-check-label" for="newPaused">
<i class="bi bi-pause-circle-fill text-warning"></i> <strong>In Pausa</strong>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="newState" id="newStopped"
checked="@(settings.DefaultNewAuctionState == "Stopped")"
@onclick="@(() => SetNewAuctionState("Stopped"))" />
<label class="form-check-label" for="newStopped">
<i class="bi bi-stop-circle-fill text-danger"></i> <strong>Fermata</strong> (default)
</label>
</div>
</div>
</div>
<div class="alert alert-info border-0 shadow-sm mt-3">
<div class="d-flex align-items-start">
<i class="bi bi-lightbulb-fill me-3 mt-1" style="font-size: 1.5rem;"></i>
<div>
<strong>Raccomandazioni:</strong>
<ul class="mb-0 mt-2">
<li><strong>Principianti:</strong> Usa "Fermate" - configura prima di avviare</li>
<li><strong>Intermedi:</strong> Usa "In Pausa" - monitora senza puntare</li>
<li><strong>Avanzati:</strong> Usa "Ricorda Stato" se configurato</li>
</ul>
</div>
</div>
</div>
<button class="btn btn-warning text-dark hover-lift" @onclick="SaveSettings">
<i class="bi bi-check-lg"></i> Salva Comportamento Avvio
</button>
</div>
</div>
<!-- IMPOSTAZIONI PREDEFINITE ASTE -->
<div class="card mb-4 shadow-hover animate-fade-in-up delay-200">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-sliders"></i> Impostazioni Predefinite Aste</h5>
@@ -95,43 +193,63 @@
<label class="form-label fw-bold">
<i class="bi bi-speedometer2"></i> Anticipo Puntata (ms):
</label>
<input type="number" class="form-control transition-colors" @bind="settings.DefaultBidBeforeDeadlineMs" />
<input type="number" class="form-control" @bind="settings.DefaultBidBeforeDeadlineMs" />
<small class="form-text text-muted">
<i class="bi bi-clock"></i> Millisecondi prima della scadenza per inviare la puntata
<i class="bi bi-clock"></i> Millisecondi prima della scadenza
</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
<i class="bi bi-currency-euro"></i> Prezzo Minimo (€):
</label>
<input type="number" step="0.01" class="form-control transition-colors" @bind="settings.DefaultMinPrice" />
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
<i class="bi bi-currency-euro"></i> Prezzo Massimo (€):
</label>
<input type="number" step="0.01" class="form-control transition-colors" @bind="settings.DefaultMaxPrice" />
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
<i class="bi bi-hand-index-thumb"></i> Click Massimi:
</label>
<input type="number" class="form-control transition-colors" @bind="settings.DefaultMaxClicks" />
<input type="number" class="form-control" @bind="settings.DefaultMaxClicks" />
<small class="form-text text-muted">
<i class="bi bi-infinity"></i> 0 = illimitati
</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
<i class="bi bi-currency-euro"></i> Prezzo Minimo (€):
</label>
<input type="number" step="0.01" class="form-control" @bind="settings.DefaultMinPrice" />
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
<i class="bi bi-currency-euro"></i> Prezzo Massimo (€):
</label>
<input type="number" step="0.01" class="form-control" @bind="settings.DefaultMaxPrice" />
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
<i class="bi bi-arrow-repeat"></i> Reset Minimi:
</label>
<input type="number" class="form-control" @bind="settings.DefaultMinResets" />
<small class="form-text text-muted">
<i class="bi bi-clock"></i> Punta solo se almeno X reset (0 = ignora)
</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
<i class="bi bi-arrow-repeat"></i> Reset Massimi:
</label>
<input type="number" class="form-control" @bind="settings.DefaultMaxResets" />
<small class="form-text text-muted">
<i class="bi bi-stop-circle"></i> Ferma dopo X reset (0 = illimitati)
</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
<i class="bi bi-shield-check"></i> Puntate Minime da Mantenere:
</label>
<input type="number" class="form-control transition-colors" @bind="settings.MinimumRemainingBids" />
<input type="number" class="form-control" @bind="settings.MinimumRemainingBids" />
<small class="form-text text-muted">
<i class="bi bi-lock"></i> Blocca puntate automatiche sotto questo limite
<i class="bi bi-lock"></i> Blocca puntate sotto questo limite
</small>
</div>
@@ -139,7 +257,7 @@
<div class="form-check">
<input type="checkbox" class="form-check-input" id="checkAuction" @bind="settings.DefaultCheckAuctionOpenBeforeBid" />
<label class="form-check-label" for="checkAuction">
<i class="bi bi-shield-fill-check"></i> Verifica asta aperta prima di puntare (default)
<i class="bi bi-shield-fill-check"></i> Verifica asta aperta prima di puntare
</label>
</div>
</div>
@@ -151,6 +269,7 @@
</div>
</div>
<!-- LIMITI LOG -->
<div class="card shadow-hover animate-fade-in-up delay-300">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-journal-text"></i> Limiti Log</h5>
@@ -161,26 +280,35 @@
<label class="form-label fw-bold">
<i class="bi bi-list-ul"></i> Righe Log Globale:
</label>
<input type="number" class="form-control transition-colors" @bind="settings.MaxGlobalLogLines" />
<input type="number" class="form-control" @bind="settings.MaxGlobalLogLines" />
<small class="form-text text-muted">
Numero massimo di righe nel log principale
</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
<i class="bi bi-list-check"></i> Righe Log per Asta:
</label>
<input type="number" class="form-control transition-colors" @bind="settings.MaxLogLinesPerAuction" />
<input type="number" class="form-control" @bind="settings.MaxLogLinesPerAuction" />
<small class="form-text text-muted">
Numero massimo di righe per ogni asta
</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">
<i class="bi bi-clock-history"></i> Voci Storia Puntate:
</label>
<input type="number" class="form-control transition-colors" @bind="settings.MaxBidHistoryEntries" />
<input type="number" class="form-control" @bind="settings.MaxBidHistoryEntries" />
<small class="form-text text-muted">
Numero massimo di puntate da mantenere (0 = illimitate)
</small>
</div>
</div>
<button class="btn btn-info text-white hover-lift" @onclick="SaveSettings">
<i class="bi bi-check-lg"></i> Salva Impostazioni
<i class="bi bi-check-lg"></i> Salva Limiti Log
</button>
</div>
</div>
@@ -191,22 +319,6 @@
max-width: 1200px;
margin: 0 auto;
}
.card {
border: none;
border-radius: 12px;
overflow: hidden;
}
.card-header {
font-weight: 600;
padding: 1.25rem;
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
}
.card-body {
padding: 1.5rem;
}
</style>
@code {
@@ -224,7 +336,6 @@
LoadSession();
LoadSettings();
// Auto-refresh dati utente ogni 30 secondi
updateTimer = new System.Threading.Timer(async _ =>
{
if (!string.IsNullOrEmpty(currentUsername))
@@ -237,7 +348,6 @@
private void LoadSession()
{
// Carica sessione salvata
var savedSession = AutoBidder.Services.SessionManager.LoadSession();
if (savedSession != null && savedSession.IsValid)
{
@@ -245,7 +355,6 @@
remainingBids = savedSession.RemainingBids;
cookieInput = savedSession.CookieString ?? "";
// Inizializza il monitor con la sessione
if (!string.IsNullOrEmpty(savedSession.CookieString))
{
AuctionMonitor.InitializeSessionWithCookie(savedSession.CookieString, savedSession.Username ?? "");
@@ -253,7 +362,6 @@
}
else
{
// Prova a caricare da AuctionMonitor
var session = AuctionMonitor.GetSession();
currentUsername = session?.Username;
remainingBids = session?.RemainingBids ?? 0;
@@ -283,7 +391,6 @@
var session = AuctionMonitor.GetSession();
if (session != null && !string.IsNullOrEmpty(session.Username))
{
// Salva la sessione
AutoBidder.Services.SessionManager.SaveSession(session);
LoadSession();
@@ -291,21 +398,21 @@
usernameInput = "";
connectionError = null;
await JSRuntime.InvokeVoidAsync("alert", $"? Connesso con successo come {session.Username}!");
await JSRuntime.InvokeVoidAsync("alert", $"? Connesso come {session.Username}!");
}
else
{
connectionError = "Sessione creata ma dati utente non disponibili. Verifica il cookie.";
connectionError = "Sessione creata ma dati utente non disponibili.";
}
}
else
{
connectionError = "Impossibile connettersi. Verifica che il cookie sia corretto e non scaduto.";
connectionError = "Impossibile connettersi. Verifica il cookie.";
}
}
catch (Exception ex)
{
connectionError = $"Errore durante la connessione: {ex.Message}";
connectionError = $"Errore: {ex.Message}";
}
finally
{
@@ -334,16 +441,11 @@
{
currentUsername = session.Username;
remainingBids = session.RemainingBids;
// Aggiorna sessione salvata
AutoBidder.Services.SessionManager.SaveSession(session);
}
}
}
catch
{
// Ignora errori di refresh silenziosamente
}
catch { }
}
private void SaveSettings()
@@ -351,6 +453,26 @@
AutoBidder.Utilities.SettingsManager.Save(settings);
_ = JSRuntime.InvokeVoidAsync("alert", "? Impostazioni salvate con successo!");
}
private void SetRememberState(bool remember)
{
settings.RememberAuctionStates = remember;
if (remember)
{
settings.DefaultStartAuctionsOnLoad = "Stopped";
}
}
private void SetLoadState(string state)
{
settings.RememberAuctionStates = false;
settings.DefaultStartAuctionsOnLoad = state;
}
private void SetNewAuctionState(string state)
{
settings.DefaultNewAuctionState = state;
}
private string GetRemainingBidsClass()
{
@@ -364,5 +486,3 @@
updateTimer?.Dispose();
}
}
@implements IDisposable

View File

@@ -172,7 +172,16 @@
protected override async Task OnInitializedAsync()
{
await RefreshStats();
try
{
await RefreshStats();
}
catch (Exception ex)
{
errorMessage = $"Errore inizializzazione: {ex.Message}";
stats = new List<ProductStat>();
Console.WriteLine($"[ERROR] Statistics OnInitializedAsync: {ex}");
}
}
private async Task RefreshStats()
@@ -183,13 +192,27 @@
errorMessage = null;
StateHasChanged();
// Tentativo caricamento con timeout
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
stats = await StatsService.GetAllStatsAsync();
if (stats == null)
{
stats = new List<ProductStat>();
errorMessage = "Nessuna statistica disponibile. Il database potrebbe non essere inizializzato.";
}
}
catch (TaskCanceledException)
{
errorMessage = "Timeout durante caricamento statistiche. Riprova più tardi.";
stats = new List<ProductStat>();
}
catch (Exception ex)
{
errorMessage = $"Si è verificato un errore: {ex.Message}";
stats = new List<ProductStat>();
Console.WriteLine($"[ERROR] Statistics page: {ex}");
Console.WriteLine($"[ERROR] Statistics RefreshStats: {ex}");
Console.WriteLine($"[ERROR] Stack trace: {ex.StackTrace}");
}
finally
{

View File

@@ -11,9 +11,8 @@
<base href="~/" />
<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.css" rel="stylesheet" />
<link href="css/app-wpf.css" rel="stylesheet" />
<link href="css/animations.css" rel="stylesheet" />
<link href="AutoBidder.styles.css" rel="stylesheet" />
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
<body>
@@ -27,11 +26,10 @@
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>
<a class="dismiss">?</a>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/browser-interop.js"></script>
<script src="_framework/blazor.server.js"></script>
</body>
</html>

View File

@@ -9,7 +9,6 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link href="css/animations.css" rel="stylesheet" />
<link href="AutoBidder.styles.css" rel="stylesheet" />
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
<body>
@@ -23,7 +22,7 @@
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>
<a class="dismiss">?</a>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>

View File

@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.DataProtection;
using System.Data.Common;
var builder = WebApplication.CreateBuilder(args);
@@ -81,6 +82,7 @@ htmlCacheService.OnLog += (msg) => Console.WriteLine(msg);
builder.Services.AddSingleton(auctionMonitor);
builder.Services.AddSingleton(htmlCacheService);
builder.Services.AddSingleton(sp => new SessionService(auctionMonitor.GetApiClient()));
builder.Services.AddSingleton<DatabaseService>();
builder.Services.AddScoped<StatsService>(sp =>
{
var ctx = sp.GetRequiredService<StatisticsContext>();
@@ -97,11 +99,82 @@ builder.Services.AddSignalR(options =>
var app = builder.Build();
// Crea database se non esiste (senza migrations)
// ??? NUOVO: Inizializza DatabaseService
using (var scope = app.Services.CreateScope())
{
var databaseService = scope.ServiceProvider.GetRequiredService<DatabaseService>();
try
{
Console.WriteLine("[DB] Initializing main database...");
await databaseService.InitializeDatabaseAsync();
var dbInfo = await databaseService.GetDatabaseInfoAsync();
Console.WriteLine($"[DB] Database initialized successfully:");
Console.WriteLine($"[DB] Path: {dbInfo.Path}");
Console.WriteLine($"[DB] Size: {dbInfo.SizeFormatted}");
Console.WriteLine($"[DB] Version: {dbInfo.Version}");
Console.WriteLine($"[DB] Auctions: {dbInfo.AuctionsCount}");
Console.WriteLine($"[DB] Bid History: {dbInfo.BidHistoryCount}");
Console.WriteLine($"[DB] Product Stats: {dbInfo.ProductStatsCount}");
// Verifica salute database
var isHealthy = await databaseService.CheckDatabaseHealthAsync();
Console.WriteLine($"[DB] Database health check: {(isHealthy ? "OK" : "FAILED")}");
}
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>();
db.Database.EnsureCreated(); // Crea schema automaticamente se non esiste
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
try
{
Console.WriteLine("[STATS DB] Attempting forced recreation...");
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
Console.WriteLine("[STATS DB] Forced recreation successful");
}
catch (Exception ex2)
{
Console.WriteLine($"[STATS DB ERROR] Forced recreation failed: {ex2.Message}");
}
}
}
// Configure the HTTP request pipeline

View File

@@ -298,6 +298,39 @@ namespace AutoBidder.Services
OnAuctionUpdated?.Invoke(state);
UpdateAuctionHistory(auction, state);
// ?? NUOVO: Calcola e aggiorna valore prodotto se disponibili dati
if (auction.BuyNowPrice.HasValue || auction.ShippingCost.HasValue)
{
try
{
var productValue = Utilities.ProductValueCalculator.Calculate(
auction,
state.Price,
auction.RecentBids?.Count ?? 0
);
auction.CalculatedValue = productValue;
// Log valore solo se cambia significativamente o ogni 10 polling
bool shouldLogValue = false;
if (auction.PollingLatencyMs % 10 == 0) // Ogni ~10 poll
{
shouldLogValue = true;
}
if (shouldLogValue && productValue.Savings.HasValue)
{
var valueMsg = Utilities.ProductValueCalculator.FormatValueMessage(productValue);
auction.AddLog($"[VALUE] {valueMsg}");
}
}
catch (Exception ex)
{
// Silenzioso - non vogliamo bloccare il polling per errori di calcolo
auction.AddLog($"[WARN] Errore calcolo valore: {ex.Message}");
}
}
// NUOVA LOGICA: Punta solo se siamo vicini alla deadline E nessun altro ha appena puntato
if (state.Status == AuctionStatus.Running && !auction.IsPaused && !auction.IsAttackInProgress)
{
@@ -439,51 +472,81 @@ namespace AutoBidder.Services
private bool ShouldBid(AuctionInfo auction, AuctionState state)
{
// ? NUOVO: Controllo limite minimo puntate residue
var settings = Utilities.SettingsManager.Load();
if (settings.MinimumRemainingBids > 0)
// ?? CONTROLLO 0: Verifica convenienza (se dati disponibili)
if (auction.CalculatedValue != null &&
auction.CalculatedValue.Savings.HasValue &&
!auction.CalculatedValue.IsWorthIt)
{
// Ottieni puntate residue dalla sessione
var session = _apiClient.GetSession();
if (session != null && session.RemainingBids <= settings.MinimumRemainingBids)
// Permetti comunque di puntare se il risparmio è ancora positivo (anche se piccolo)
// Blocca solo se sta andando in perdita significativa (< -5%)
if (auction.CalculatedValue.SavingsPercentage.HasValue &&
auction.CalculatedValue.SavingsPercentage.Value < -5)
{
auction.AddLog($"[LIMIT] Puntata bloccata: puntate residue ({session.RemainingBids}) al limite minimo ({settings.MinimumRemainingBids})");
auction.AddLog($"[VALUE] Puntata bloccata: perdita {auction.CalculatedValue.SavingsPercentage.Value:F1}% (troppo costoso)");
return false;
}
}
// ? NUOVO: Non puntare se sono già il vincitore corrente
// ??? CONTROLLO 1: Limite minimo puntate residue
var settings = Utilities.SettingsManager.Load();
if (settings.MinimumRemainingBids > 0)
{
var session = _apiClient.GetSession();
if (session != null && session.RemainingBids <= settings.MinimumRemainingBids)
{
auction.AddLog($"[LIMIT] Puntata bloccata: puntate residue ({session.RemainingBids}) <= limite ({settings.MinimumRemainingBids})");
return false;
}
}
// ? CONTROLLO 2: Non puntare se sono già il vincitore corrente
if (state.IsMyBid)
{
// Sono già io l'ultimo ad aver puntato, non serve puntare di nuovo
return false;
}
// Price check
// ?? CONTROLLO 3: MinPrice/MaxPrice
if (auction.MinPrice > 0 && state.Price < auction.MinPrice)
{
auction.AddLog($"[PRICE] Prezzo troppo basso: €{state.Price:F2} < Min €{auction.MinPrice:F2}");
return false;
}
if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice)
{
auction.AddLog($"[PRICE] Prezzo troppo alto: €{state.Price:F2} > Max €{auction.MaxPrice:F2}");
return false;
}
// Reset count check
// ?? CONTROLLO 4: MinResets/MaxResets
if (auction.MinResets > 0 && auction.ResetCount < auction.MinResets)
{
auction.AddLog($"[RESET] Reset troppo bassi: {auction.ResetCount} < Min {auction.MinResets}");
return false;
}
if (auction.MaxResets > 0 && auction.ResetCount >= auction.MaxResets)
{
auction.AddLog($"[RESET] Reset massimi raggiunti: {auction.ResetCount} >= Max {auction.MaxResets}");
return false;
}
// Max clicks check
// ??? CONTROLLO 5: MaxClicks
int myBidsCount = auction.BidHistory.Count(b => b.EventType == BidEventType.MyBid);
if (auction.MaxClicks > 0 && myBidsCount >= auction.MaxClicks)
{
auction.AddLog($"[CLICKS] Click massimi raggiunti: {myBidsCount} >= Max {auction.MaxClicks}");
return false;
}
// Cooldown check (evita puntate multiple ravvicinate)
// ?? CONTROLLO 6: Cooldown (evita puntate multiple ravvicinate)
if (auction.LastClickAt.HasValue)
{
var timeSinceLastClick = DateTime.UtcNow - auction.LastClickAt.Value;
if (timeSinceLastClick.TotalMilliseconds < 800)
{
return false;
}
}
return true;

View File

@@ -0,0 +1,414 @@
using Microsoft.Data.Sqlite;
using System;
using System.IO;
using System.Threading.Tasks;
namespace AutoBidder.Services
{
/// <summary>
/// Servizio per gestione database SQLite con auto-creazione tabelle e migrations
/// </summary>
public class DatabaseService : IDisposable
{
private readonly string _connectionString;
private readonly string _databasePath;
private SqliteConnection? _connection;
public DatabaseService()
{
// Crea directory data se non esiste
var dataDir = Path.Combine(AppContext.BaseDirectory, "data");
Directory.CreateDirectory(dataDir);
_databasePath = Path.Combine(dataDir, "autobidder.db");
_connectionString = $"Data Source={_databasePath}";
}
/// <summary>
/// Inizializza il database creando le tabelle se necessario
/// </summary>
public async Task InitializeDatabaseAsync()
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
// Abilita foreign keys
await using (var cmd = connection.CreateCommand())
{
cmd.CommandText = "PRAGMA foreign_keys = ON;";
await cmd.ExecuteNonQueryAsync();
}
// Crea tabelle se non esistono
await CreateTablesAsync(connection);
// Esegui migrations
await RunMigrationsAsync(connection);
}
/// <summary>
/// Crea tutte le tabelle necessarie
/// </summary>
private async Task CreateTablesAsync(SqliteConnection connection)
{
var createTablesSql = @"
-- Tabella versione database per migrations
CREATE TABLE IF NOT EXISTS DatabaseVersion (
Version INTEGER PRIMARY KEY,
AppliedAt TEXT NOT NULL,
Description TEXT
);
-- Tabella aste monitorate
CREATE TABLE IF NOT EXISTS Auctions (
AuctionId TEXT PRIMARY KEY,
Name TEXT NOT NULL,
OriginalUrl TEXT NOT NULL,
BidBeforeDeadlineMs INTEGER NOT NULL DEFAULT 200,
CheckAuctionOpenBeforeBid INTEGER NOT NULL DEFAULT 0,
MinPrice REAL NOT NULL DEFAULT 0,
MaxPrice REAL NOT NULL DEFAULT 0,
MinResets INTEGER NOT NULL DEFAULT 0,
MaxResets INTEGER NOT NULL DEFAULT 0,
MaxClicks INTEGER NOT NULL DEFAULT 0,
IsActive INTEGER NOT NULL DEFAULT 1,
IsPaused INTEGER NOT NULL DEFAULT 0,
ResetCount INTEGER NOT NULL DEFAULT 0,
RemainingBids INTEGER,
BidsUsedOnThisAuction INTEGER,
BuyNowPrice REAL,
ShippingCost REAL,
HasWinLimit INTEGER NOT NULL DEFAULT 0,
WinLimitDescription TEXT,
BidCost REAL NOT NULL DEFAULT 0.20,
AddedAt TEXT NOT NULL,
LastClickAt TEXT,
CreatedAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UpdatedAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Tabella storia puntate
CREATE TABLE IF NOT EXISTS BidHistory (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
AuctionId TEXT NOT NULL,
Timestamp TEXT NOT NULL,
EventType INTEGER NOT NULL,
Bidder TEXT,
Price REAL NOT NULL,
Timer REAL NOT NULL,
LatencyMs INTEGER,
Success INTEGER,
Notes TEXT,
FOREIGN KEY (AuctionId) REFERENCES Auctions(AuctionId) ON DELETE CASCADE
);
-- Tabella statistiche puntatori
CREATE TABLE IF NOT EXISTS BidderStats (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
AuctionId TEXT NOT NULL,
Username TEXT NOT NULL,
BidCount INTEGER NOT NULL DEFAULT 0,
LastBidTime TEXT,
FOREIGN KEY (AuctionId) REFERENCES Auctions(AuctionId) ON DELETE CASCADE,
UNIQUE(AuctionId, Username)
);
-- Tabella log aste
CREATE TABLE IF NOT EXISTS AuctionLogs (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
AuctionId TEXT NOT NULL,
Timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
LogLevel TEXT NOT NULL,
Message TEXT NOT NULL,
FOREIGN KEY (AuctionId) REFERENCES Auctions(AuctionId) ON DELETE CASCADE
);
-- Tabella statistiche prodotti (già esistente da StatsService)
CREATE TABLE IF NOT EXISTS ProductStats (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
ProductKey TEXT NOT NULL,
ProductName TEXT NOT NULL,
TotalAuctions INTEGER NOT NULL DEFAULT 0,
TotalBidsUsed INTEGER NOT NULL DEFAULT 0,
TotalFinalPriceCents INTEGER NOT NULL DEFAULT 0,
LastSeen TEXT NOT NULL,
UNIQUE(ProductKey)
);
-- Indici per performance
CREATE INDEX IF NOT EXISTS idx_auctions_isactive ON Auctions(IsActive);
CREATE INDEX IF NOT EXISTS idx_bidhistory_auctionid ON BidHistory(AuctionId);
CREATE INDEX IF NOT EXISTS idx_bidhistory_timestamp ON BidHistory(Timestamp);
CREATE INDEX IF NOT EXISTS idx_bidderstats_auctionid ON BidderStats(AuctionId);
CREATE INDEX IF NOT EXISTS idx_auctionlogs_auctionid ON AuctionLogs(AuctionId);
CREATE INDEX IF NOT EXISTS idx_auctionlogs_timestamp ON AuctionLogs(Timestamp);
CREATE INDEX IF NOT EXISTS idx_productstats_productkey ON ProductStats(ProductKey);
";
await using var cmd = connection.CreateCommand();
cmd.CommandText = createTablesSql;
await cmd.ExecuteNonQueryAsync();
}
/// <summary>
/// Esegue migrations del database
/// </summary>
private async Task RunMigrationsAsync(SqliteConnection connection)
{
var currentVersion = await GetDatabaseVersionAsync(connection);
// Migrations in ordine crescente
var migrations = new[]
{
new Migration(1, "Initial schema", async (conn) => {
// Schema già creato in CreateTablesAsync
await Task.CompletedTask;
}),
new Migration(2, "Add UpdatedAt triggers", async (conn) => {
var sql = @"
-- Trigger per aggiornare UpdatedAt automaticamente
CREATE TRIGGER IF NOT EXISTS update_auctions_timestamp
AFTER UPDATE ON Auctions
BEGIN
UPDATE Auctions SET UpdatedAt = CURRENT_TIMESTAMP WHERE AuctionId = NEW.AuctionId;
END;
";
await using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync();
}),
new Migration(3, "Add BidHistory indexes optimization", async (conn) => {
var sql = @"
CREATE INDEX IF NOT EXISTS idx_bidhistory_composite ON BidHistory(AuctionId, Timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_bidderstats_composite ON BidderStats(AuctionId, Username);
";
await using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync();
})
};
foreach (var migration in migrations)
{
if (migration.Version > currentVersion)
{
await migration.Execute(connection);
await SetDatabaseVersionAsync(connection, migration.Version, migration.Description);
}
}
}
/// <summary>
/// Ottiene la versione corrente del database
/// </summary>
private async Task<int> GetDatabaseVersionAsync(SqliteConnection connection)
{
await using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT COALESCE(MAX(Version), 0) FROM DatabaseVersion;";
var result = await cmd.ExecuteScalarAsync();
return Convert.ToInt32(result);
}
/// <summary>
/// Imposta la versione del database dopo una migration
/// </summary>
private async Task SetDatabaseVersionAsync(SqliteConnection connection, int version, string description)
{
await using var cmd = connection.CreateCommand();
cmd.CommandText = @"
INSERT INTO DatabaseVersion (Version, AppliedAt, Description)
VALUES (@version, @appliedAt, @description);
";
cmd.Parameters.AddWithValue("@version", version);
cmd.Parameters.AddWithValue("@appliedAt", DateTime.UtcNow.ToString("O"));
cmd.Parameters.AddWithValue("@description", description);
await cmd.ExecuteNonQueryAsync();
}
/// <summary>
/// Ottiene una connessione al database
/// </summary>
public async Task<SqliteConnection> GetConnectionAsync()
{
var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
return connection;
}
/// <summary>
/// Esegue una query SQL e ritorna il numero di righe affette
/// </summary>
public async Task<int> ExecuteNonQueryAsync(string sql, params SqliteParameter[] parameters)
{
await using var connection = await GetConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = sql;
if (parameters != null)
{
cmd.Parameters.AddRange(parameters);
}
return await cmd.ExecuteNonQueryAsync();
}
/// <summary>
/// Esegue una query SQL e ritorna un valore scalare
/// </summary>
public async Task<object?> ExecuteScalarAsync(string sql, params SqliteParameter[] parameters)
{
await using var connection = await GetConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = sql;
if (parameters != null)
{
cmd.Parameters.AddRange(parameters);
}
return await cmd.ExecuteScalarAsync();
}
/// <summary>
/// Verifica la salute del database
/// </summary>
public async Task<bool> CheckDatabaseHealthAsync()
{
try
{
await using var connection = await GetConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type='table';";
var result = await cmd.ExecuteScalarAsync();
return Convert.ToInt32(result) > 0;
}
catch
{
return false;
}
}
/// <summary>
/// Ottiene informazioni sul database
/// </summary>
public async Task<DatabaseInfo> GetDatabaseInfoAsync()
{
await using var connection = await GetConnectionAsync();
var info = new DatabaseInfo
{
Path = _databasePath,
SizeBytes = new FileInfo(_databasePath).Length,
Version = await GetDatabaseVersionAsync(connection)
};
// Conta record per tabella
await using var cmd = connection.CreateCommand();
cmd.CommandText = @"
SELECT
(SELECT COUNT(*) FROM Auctions) as AuctionsCount,
(SELECT COUNT(*) FROM BidHistory) as BidHistoryCount,
(SELECT COUNT(*) FROM BidderStats) as BidderStatsCount,
(SELECT COUNT(*) FROM AuctionLogs) as AuctionLogsCount,
(SELECT COUNT(*) FROM ProductStats) as ProductStatsCount;
";
await using var reader = await cmd.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
info.AuctionsCount = reader.GetInt32(0);
info.BidHistoryCount = reader.GetInt32(1);
info.BidderStatsCount = reader.GetInt32(2);
info.AuctionLogsCount = reader.GetInt32(3);
info.ProductStatsCount = reader.GetInt32(4);
}
return info;
}
/// <summary>
/// Ottimizza il database (VACUUM)
/// </summary>
public async Task OptimizeDatabaseAsync()
{
await using var connection = await GetConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = "VACUUM;";
await cmd.ExecuteNonQueryAsync();
}
/// <summary>
/// Backup del database
/// </summary>
public async Task<string> BackupDatabaseAsync()
{
var backupDir = Path.Combine(AppContext.BaseDirectory, "data", "backups");
Directory.CreateDirectory(backupDir);
var backupPath = Path.Combine(backupDir, $"autobidder_backup_{DateTime.Now:yyyyMMdd_HHmmss}.db");
await using var source = await GetConnectionAsync();
await using var destination = new SqliteConnection($"Data Source={backupPath}");
await destination.OpenAsync();
source.BackupDatabase(destination);
return backupPath;
}
public void Dispose()
{
_connection?.Dispose();
}
/// <summary>
/// Classe per rappresentare una migration
/// </summary>
private class Migration
{
public int Version { get; }
public string Description { get; }
private readonly Func<SqliteConnection, Task> _execute;
public Migration(int version, string description, Func<SqliteConnection, Task> execute)
{
Version = version;
Description = description;
_execute = execute;
}
public async Task Execute(SqliteConnection connection)
{
await _execute(connection);
}
}
}
/// <summary>
/// Informazioni sul database
/// </summary>
public class DatabaseInfo
{
public string Path { get; set; } = "";
public long SizeBytes { get; set; }
public int Version { get; set; }
public int AuctionsCount { get; set; }
public int BidHistoryCount { get; set; }
public int BidderStatsCount { get; set; }
public int AuctionLogsCount { get; set; }
public int ProductStatsCount { get; set; }
public string SizeFormatted => FormatBytes(SizeBytes);
private static string FormatBytes(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}
}

View File

@@ -15,8 +15,30 @@ namespace AutoBidder.Services
public StatsService(StatisticsContext ctx)
{
_ctx = ctx;
// Assicurati che il database esista (senza migrations)
_ctx.Database.EnsureCreated();
// Assicurati che il database esista
try
{
_ctx.Database.EnsureCreated();
// Verifica che la tabella ProductStats esista
var canQuery = _ctx.ProductStats.Any();
}
catch (Exception ex)
{
Console.WriteLine($"[StatsService] Database initialization failed: {ex.Message}");
// Prova a ricreare
try
{
_ctx.Database.EnsureDeleted();
_ctx.Database.EnsureCreated();
Console.WriteLine("[StatsService] Database recreated successfully");
}
catch (Exception ex2)
{
Console.WriteLine($"[StatsService] Database recreation failed: {ex2.Message}");
}
}
}
private static string NormalizeKey(string? productName, string? auctionUrl)
@@ -101,9 +123,42 @@ namespace AutoBidder.Services
// New: return all stats for export
public async Task<List<ProductStat>> GetAllStatsAsync()
{
return await _ctx.ProductStats
.OrderByDescending(p => p.LastSeen)
.ToListAsync();
try
{
// Verifica che la tabella esista prima di fare query
if (!_ctx.Database.CanConnect())
{
Console.WriteLine("[StatsService] Database not available, returning empty list");
return new List<ProductStat>();
}
return await _ctx.ProductStats
.OrderByDescending(p => p.LastSeen)
.ToListAsync();
}
catch (Microsoft.Data.Sqlite.SqliteException ex) when (ex.SqliteErrorCode == 1)
{
// Table doesn't exist - return empty list
Console.WriteLine($"[StatsService] Table doesn't exist: {ex.Message}");
// Try to create schema
try
{
_ctx.Database.EnsureCreated();
Console.WriteLine("[StatsService] Schema created, returning empty list");
}
catch (Exception ex2)
{
Console.WriteLine($"[StatsService] Failed to create schema: {ex2.Message}");
}
return new List<ProductStat>();
}
catch (Exception ex)
{
Console.WriteLine($"[StatsService] GetAllStatsAsync failed: {ex.Message}");
return new List<ProductStat>();
}
}
}
}

View File

@@ -24,5 +24,5 @@
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>
<a class="dismiss">?</a>
</div>

View File

@@ -17,11 +17,6 @@
<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="browser">
<i class="bi bi-globe me-2"></i> Browser
</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

View File

@@ -1,127 +1,102 @@
@inject AuctionMonitor AuctionMonitor
@implements IDisposable
<div class="user-banner animate-fade-in">
<div class="user-stats-banner">
@if (!string.IsNullOrEmpty(username))
{
<div class="user-info animate-scale-in">
<div class="user-avatar">
<i class="bi bi-person-circle" style="font-size: 2rem;"></i>
<div class="stats-row">
<div class="stat-item stat-bids">
<span class="stat-label">Puntate:</span>
<span class="stat-value @GetBidsColorClass()">@remainingBids</span>
</div>
<div class="user-details">
<div class="username">
<i class="bi bi-person-fill me-1"></i>@username
</div>
<div class="remaining-bids mt-1">
<span class="badge @GetBidsColorClass() badge-pulse">
<i class="bi bi-hand-index-fill me-1"></i>@remainingBids puntate
</span>
</div>
<div class="stat-separator">|</div>
<div class="stat-item stat-credit">
<span class="stat-label">Credito Shop:</span>
<span class="stat-value text-success">EUR @shopCredit.ToString("F2")</span>
</div>
<div class="connection-status ms-3">
<span class="badge bg-success animate-glow">
<i class="bi bi-wifi"></i> Connesso
</span>
<div class="stat-separator">|</div>
<div class="stat-item stat-wins">
<span class="stat-label">Aste vinte da confermare:</span>
<span class="stat-value text-warning">@auctionsWon</span>
</div>
</div>
}
else
{
<div class="user-info animate-fade-in">
<div class="user-avatar opacity-50">
<i class="bi bi-person-x-fill" style="font-size: 2rem;"></i>
</div>
<div class="user-details">
<span class="text-light opacity-75">
<i class="bi bi-x-circle me-1"></i>Non connesso
<div class="stats-row">
<div class="stat-item">
<span class="text-muted">
<i class="bi bi-x-circle me-2"></i>Non connesso
</span>
</div>
<div class="connection-status ms-3">
<a href="/settings" class="btn btn-sm btn-outline-light hover-lift">
<i class="bi bi-box-arrow-in-right"></i> Connetti
</a>
</div>
</div>
}
</div>
<style>
.user-banner {
padding: 1rem;
background: linear-gradient(135deg, rgba(13, 110, 253, 0.1), rgba(13, 202, 240, 0.1));
border-radius: 10px;
margin: 0.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.user-stats-banner {
background: #1e1e1e;
border-bottom: 1px solid #3e3e42;
padding: 0.5rem 1rem;
font-size: 0.9rem;
}
.user-info {
.stats-row {
display: flex;
align-items: center;
gap: 1rem;
color: #cccccc;
}
.user-avatar {
color: white;
transition: all 0.3s ease;
}
.user-avatar:hover {
transform: scale(1.1) rotate(5deg);
}
.user-details {
.stat-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
align-items: center;
gap: 0.5rem;
}
.username {
font-weight: 600;
color: white;
font-size: 1.1rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.remaining-bids .badge {
font-size: 0.9rem;
padding: 0.35rem 0.75rem;
border-radius: 20px;
.stat-label {
color: #8b949e;
font-weight: 500;
}
.connection-status .badge {
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: 500;
.stat-value {
font-weight: 700;
font-size: 1rem;
}
.badge-low-bids {
background: linear-gradient(135deg, #dc3545, #c82333);
animation: pulse 1s ease-in-out infinite;
.stat-separator {
color: #3e3e42;
font-weight: bold;
}
.badge-medium-bids {
background: linear-gradient(135deg, #ffc107, #ff9800);
.bids-low {
color: #f85149 !important;
}
.badge-high-bids {
background: linear-gradient(135deg, #28a745, #20c997);
.bids-medium {
color: #d29922 !important;
}
.bids-high {
color: #3fb950 !important;
}
</style>
@code {
private string? username;
private int remainingBids;
private double shopCredit;
private int auctionsWon;
private System.Threading.Timer? updateTimer;
protected override void OnInitialized()
{
_ = UpdateUserInfo(); // Fire-and-forget è intenzionale qui
_ = UpdateUserInfo();
updateTimer = new System.Threading.Timer(async _ =>
{
await UpdateUserInfo();
await InvokeAsync(StateHasChanged);
}, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
private async Task UpdateUserInfo()
@@ -129,13 +104,15 @@
var session = AuctionMonitor.GetSession();
username = session?.Username;
remainingBids = session?.RemainingBids ?? 0;
shopCredit = session?.ShopCredit ?? 0;
auctionsWon = 0; // TODO: aggiungere campo al model BidooSession
}
private string GetBidsColorClass()
{
if (remainingBids < 50) return "badge-low-bids";
if (remainingBids < 150) return "badge-medium-bids";
return "badge-high-bids";
if (remainingBids < 50) return "bids-low";
if (remainingBids < 150) return "bids-medium";
return "bids-high";
}
public void Dispose()

View File

@@ -253,11 +253,11 @@ namespace AutoBidder.Utilities
{
summary += $" | Valore: {value.BuyNowPrice.Value:F2}€";
if (value.Savings.HasValue)
if (value.Savings.HasValue && value.SavingsPercentage.HasValue)
{
if (value.Savings.Value > 0)
{
summary += $" | Risparmio: {value.Savings.Value:F2}€ ({value.SavingsPercentage:F1}%)";
summary += $" | Risparmio: {value.Savings.Value:F2}€ ({value.SavingsPercentage.Value:F1}%)";
}
else
{
@@ -284,13 +284,17 @@ namespace AutoBidder.Utilities
return $"?? Costo totale: {value.TotalCostIfWin:F2}€ (prezzo: {value.CurrentPrice:F2}€ + puntate: {value.MyBidsCost:F2}€)";
}
if (value.IsWorthIt)
if (value.IsWorthIt && value.Savings.HasValue && value.SavingsPercentage.HasValue)
{
return $"? Conveniente! Risparmio: {value.Savings:F2}€ ({value.SavingsPercentage:F1}%) - Totale: {value.TotalCostIfWin:F2}€ vs {value.BuyNowPrice.Value:F2}€";
return $"? Conveniente! Risparmio: {value.Savings.Value:F2}€ ({value.SavingsPercentage.Value:F1}%) - Totale: {value.TotalCostIfWin:F2}€ vs {value.BuyNowPrice.Value:F2}€";
}
else if (value.Savings.HasValue && value.SavingsPercentage.HasValue)
{
return $"? Non conveniente! Spesa extra: {Math.Abs(value.Savings.Value):F2}€ ({Math.Abs(value.SavingsPercentage.Value):F1}%) - Totale: {value.TotalCostIfWin:F2}€ vs {value.BuyNowPrice.Value:F2}€";
}
else
{
return $"? Non conveniente! Spesa extra: {Math.Abs(value.Savings.Value):F2}€ ({Math.Abs(value.SavingsPercentage.Value):F1}%) - Totale: {value.TotalCostIfWin:F2}€ vs {value.BuyNowPrice.Value:F2}€";
return $"?? Costo totale: {value.TotalCostIfWin:F2}€";
}
}
}

View File

@@ -12,6 +12,8 @@ namespace AutoBidder.Utilities
public double DefaultMinPrice { get; set; } = 0;
public double DefaultMaxPrice { get; set; } = 0;
public int DefaultMaxClicks { get; set; } = 0;
public int DefaultMinResets { get; set; } = 0;
public int DefaultMaxResets { get; set; } = 0;
// LIMITI LOG
/// <summary>

View File

@@ -0,0 +1,49 @@
version: '3.8'
# Docker Compose per ambiente di sviluppo
# Usa: docker-compose -f docker-compose.dev.yml up
services:
autobidder-dev:
build:
context: .
dockerfile: Dockerfile
target: build # Ferma alla fase build per debugging
container_name: autobidder-dev
ports:
- "5000:5000"
- "5001:5001"
volumes:
- ./data:/app/data
- ./wwwroot:/app/wwwroot # Hot-reload static files
- ./logs:/app/logs
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://+:5000;https://+:5001
- Logging__LogLevel__Default=Debug
- Logging__LogLevel__Microsoft=Information
restart: no # No auto-restart in dev
networks:
- dev-network
stdin_open: true
tty: true
# SQLite browser per debug database
sqlite-web:
image: coleifer/sqlite-web
container_name: sqlite-web
ports:
- "8080:8080"
volumes:
- ./data:/data
environment:
- SQLITE_DATABASE=/data/autobidder.db
command: ["sqlite_web", "-H", "0.0.0.0", "/data/autobidder.db"]
networks:
- dev-network
profiles:
- debug # Avvia con: docker-compose --profile debug up
networks:
dev-network:
driver: bridge

View File

@@ -8,9 +8,29 @@ services:
container_name: autobidder
ports:
- "5000:5000"
- "5001:5001"
volumes:
- ./data:/app/data
- autobidder-keys:/root/.aspnet/DataProtection-Keys
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:5000
- ASPNETCORE_URLS=http://+:5000;https://+:5001
- ASPNETCORE_Kestrel__Certificates__Default__Path=/app/cert/autobidder.pfx
- ASPNETCORE_Kestrel__Certificates__Default__Password=${CERT_PASSWORD:-AutoBidder2024}
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- autobidder-network
volumes:
autobidder-keys:
driver: local
networks:
autobidder-network:
driver: bridge

View File

@@ -0,0 +1,559 @@
/* app-wpf.css - WPF Dark Theme + Modern Sidebar */
: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;
/* WPF Accent Colors */
--primary-color: #007acc;
--success-color: #00d800;
--warning-color: #ffb700;
--danger-color: #e81123;
--info-color: #00b7c3;
/* Log Syntax Colors */
--log-success: #00d800;
--log-warning: #ffb700;
--log-error: #f48771;
--log-info: #4ec9b0;
--log-debug: #569cd6;
--log-timestamp: #808080;
}
/* === GLOBAL === */
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg-primary);
color: var(--text-secondary);
font-size: 13px;
}
/* === LAYOUT === */
.page {
display: flex;
height: 100vh;
overflow: hidden;
}
/* Sidebar Moderna - 250px come prima */
.sidebar {
width: 250px;
height: 100vh;
position: fixed;
left: 0;
top: 0;
background: linear-gradient(180deg, #1c2128 0%, #161b22 50%, #0d1117 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;
flex: 1;
display: flex;
flex-direction: column;
height: 100vh;
}
.content {
flex: 1;
overflow: hidden;
}
/* === AUCTION MONITOR === */
.auction-monitor {
display: flex;
flex-direction: column;
height: 100%;
padding: 0.5rem;
gap: 0.5rem;
}
.toolbar {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 0.5rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.toolbar .btn {
padding: 0.4rem 1rem;
font-size: 0.875rem;
font-weight: 600;
border-radius: 3px;
}
.btn-success {
background: var(--success-color) !important;
border-color: var(--success-color) !important;
color: #000 !important;
}
.btn-warning {
background: var(--warning-color) !important;
border-color: var(--warning-color) !important;
color: #000 !important;
}
.btn-danger {
background: var(--danger-color) !important;
border-color: var(--danger-color) !important;
color: #fff !important;
}
.btn-primary {
background: var(--primary-color) !important;
border-color: var(--primary-color) !important;
color: #fff !important;
}
.btn-info {
background: var(--info-color) !important;
border-color: var(--info-color) !important;
color: #000 !important;
}
.btn-secondary {
background: var(--bg-hover) !important;
border-color: var(--border-color) !important;
color: var(--text-secondary) !important;
}
/* === GRID LAYOUT CORRETTO === */
/* Auctions List: sinistra, full height */
/* Global Log: alto destra */
/* Auction Details: basso destra */
.content-grid {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
grid-template-rows: 1fr 300px;
gap: 0.5rem;
flex: 1;
min-height: 0;
}
.auctions-list {
grid-column: 1;
grid-row: 1 / 3;
border: 1px solid var(--border-color);
background: var(--bg-secondary);
display: flex;
flex-direction: column;
overflow: hidden;
}
/* LOG GLOBALE - ALTO DESTRA */
.global-log {
grid-column: 2;
grid-row: 1;
border: 1px solid var(--border-color);
background: var(--bg-primary);
display: flex;
flex-direction: column;
overflow: hidden;
}
/* DETTAGLI ASTA - BASSO DESTRA */
.auction-details {
grid-column: 2;
grid-row: 2;
border: 1px solid var(--border-color);
background: var(--bg-secondary);
display: flex;
flex-direction: column;
overflow: auto;
}
.auctions-list h3,
.global-log h4,
.auction-details h3 {
color: var(--success-color);
font-weight: 600;
font-size: 0.9rem;
margin: 0;
padding: 0.5rem;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
}
/* === TABLES === */
.table {
color: var(--text-secondary);
font-size: 0.813rem;
margin: 0;
}
.table thead {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.table thead th {
font-weight: 600;
border: 1px solid var(--border-color);
padding: 0.4rem 0.5rem;
white-space: nowrap;
}
.table tbody tr {
background: var(--bg-primary);
border-bottom: 1px solid var(--border-color);
cursor: pointer;
}
.table tbody tr:hover {
background: var(--bg-hover);
}
.table tbody tr.table-active {
background: var(--bg-selected) !important;
color: var(--text-primary);
}
.table tbody td {
border: 1px solid var(--border-color);
padding: 0.3rem 0.5rem;
vertical-align: middle;
}
/* === LOG === */
.log-box {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
background: var(--bg-primary);
font-family: 'Consolas', 'Courier New', monospace;
font-size: 0.75rem;
line-height: 1.4;
}
.log-box div {
padding: 0.1rem 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.log-success {
color: var(--log-success);
}
.log-warning {
color: var(--log-warning);
}
.log-error {
color: var(--log-error);
}
.log-info {
color: var(--log-info);
}
.log-timestamp {
color: var(--log-timestamp);
}
/* === FORMS === */
.form-control, .form-select {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-secondary);
font-size: 0.813rem;
padding: 0.3rem 0.5rem;
border-radius: 3px;
}
.form-control:focus, .form-select:focus {
background: var(--bg-primary);
border-color: var(--primary-color);
color: var(--text-primary);
box-shadow: 0 0 0 0.2rem rgba(0, 122, 204, 0.25);
}
.form-label {
color: var(--text-secondary);
font-weight: 500;
font-size: 0.813rem;
margin-bottom: 0.25rem;
}
.form-check-input {
background-color: var(--bg-tertiary);
border-color: var(--border-color);
}
.form-check-input:checked {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
/* === BADGES === */
.badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 3px;
font-weight: 600;
}
.badge.bg-success {
background: var(--success-color) !important;
color: #000;
}
.badge.bg-warning {
background: var(--warning-color) !important;
color: #000;
}
.badge.bg-danger {
background: var(--danger-color) !important;
}
.badge.bg-info {
background: var(--info-color) !important;
color: #000;
}
.badge.bg-secondary {
background: var(--bg-hover) !important;
color: var(--text-secondary);
}
.badge.bg-primary {
background: var(--primary-color) !important;
}
/* === BUTTONS GROUP === */
.btn-group-sm .btn {
padding: 0.2rem 0.5rem;
font-size: 0.75rem;
}
/* === AUCTION DETAILS SECTIONS === */
.auction-info {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 0.75rem;
margin: 0.5rem;
}
.info-group {
margin-bottom: 0.75rem;
}
.info-group label {
display: block;
font-weight: 600;
margin-bottom: 0.25rem;
color: var(--text-secondary);
font-size: 0.813rem;
}
.auction-log, .bidders-stats {
margin: 0.5rem;
}
.auction-log h4, .bidders-stats h4 {
color: var(--success-color);
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 0.5rem;
padding-bottom: 0.25rem;
border-bottom: 1px solid var(--border-color);
}
/* === INPUT GROUPS === */
.input-group .form-control {
border-right: 1px solid var(--border-color);
}
.input-group-text {
background: var(--bg-tertiary);
border-color: var(--border-color);
color: var(--text-secondary);
font-size: 0.813rem;
}
/* === MODAL === */
.modal-content {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--text-secondary);
}
.modal-header {
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
}
.modal-footer {
background: var(--bg-tertiary);
border-top: 1px solid var(--border-color);
}
.btn-close {
filter: invert(1) grayscale(100%) brightness(200%);
}
/* === ALERTS === */
.alert {
border-radius: 3px;
border: 1px solid;
font-size: 0.813rem;
padding: 0.5rem 0.75rem;
}
.alert-info {
background: rgba(0, 183, 195, 0.1);
border-color: var(--info-color);
color: var(--info-color);
}
.alert-success {
background: rgba(0, 216, 0, 0.1);
border-color: var(--success-color);
color: var(--success-color);
}
.alert-warning {
background: rgba(255, 183, 0, 0.1);
border-color: var(--warning-color);
color: var(--warning-color);
}
.alert-danger {
background: rgba(232, 17, 35, 0.1);
border-color: var(--danger-color);
color: var(--danger-color);
}
.alert-secondary {
background: rgba(62, 62, 66, 0.1);
border-color: var(--border-color);
color: var(--text-secondary);
}
/* === CARDS === */
.card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 3px;
color: var(--text-secondary);
}
.card-header {
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
font-weight: 600;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
}
.card-body {
padding: 0.75rem;
}
/* === SCROLLBAR === */
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background: var(--bg-primary);
}
::-webkit-scrollbar-thumb {
background: var(--bg-hover);
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* === TEXT COLORS === */
.text-success {
color: var(--success-color) !important;
}
.text-warning {
color: var(--warning-color) !important;
}
.text-danger {
color: var(--danger-color) !important;
}
.text-info {
color: var(--info-color) !important;
}
.text-muted {
color: var(--text-muted) !important;
}
.text-primary {
color: var(--text-primary) !important;
}
.fw-semibold {
font-weight: 600 !important;
}
/* === RESPONSIVE === */
@media (max-width: 1200px) {
.content-grid {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto;
}
.auctions-list {
grid-column: 1;
grid-row: 1;
min-height: 300px;
}
.global-log {
grid-column: 1;
grid-row: 2;
min-height: 200px;
}
.auction-details {
grid-column: 1;
grid-row: 3;
min-height: 200px;
}
}
@media (max-width: 768px) {
.sidebar {
width: 100%;
height: auto;
position: relative;
}
main {
margin-left: 0;
}
}

File diff suppressed because it is too large Load Diff