From 29724f5baff91af3e6a37341f9a7345981247519 Mon Sep 17 00:00:00 2001 From: Alberto Balbo Date: Tue, 23 Dec 2025 21:35:44 +0100 Subject: [PATCH] Refactoring: Docker, CI/CD, tema WPF, DB avanzato, UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Mimante/.dockerignore | 113 +- Mimante/.env.example | 71 ++ Mimante/.gitea/workflows/deploy.yml | 89 ++ Mimante/.github/workflows/ci-cd.yml | 97 ++ Mimante/.gitignore | 456 ++++++++ Mimante/AutoBidder.csproj | 9 + Mimante/Data/.gitkeep | 1 + Mimante/Dockerfile | 68 +- Mimante/Makefile | 186 ++++ Mimante/Models/AuctionInfo.cs | 6 + Mimante/Pages/Browser.razor | 292 ----- Mimante/Pages/FreeBids.razor | 419 +------- Mimante/Pages/Health.razor | 35 + Mimante/Pages/Index.razor | 279 ++--- Mimante/Pages/Index.razor.cs | 348 +++++- Mimante/Pages/Settings.razor | 238 +++- Mimante/Pages/Statistics.razor | 27 +- Mimante/Pages/_Host.cshtml | 6 +- Mimante/Pages/_Layout.cshtml | 3 +- Mimante/Program.cs | 77 +- Mimante/Services/AuctionMonitor.cs | 89 +- Mimante/Services/DatabaseService.cs | 414 +++++++ Mimante/Services/StatsService.cs | 65 +- Mimante/Shared/MainLayout.razor | 2 +- Mimante/Shared/NavMenu.razor | 5 - Mimante/Shared/UserBanner.razor | 127 +-- Mimante/Utilities/ProductValueCalculator.cs | 14 +- Mimante/Utilities/SettingsManager.cs | 2 + Mimante/docker-compose.dev.yml | 49 + Mimante/docker-compose.yml | 22 +- Mimante/wwwroot/css/app-wpf.css | 559 ++++++++++ Mimante/wwwroot/css/app.css | 1073 +++++++++++-------- 32 files changed, 3761 insertions(+), 1480 deletions(-) create mode 100644 Mimante/.env.example create mode 100644 Mimante/.gitea/workflows/deploy.yml create mode 100644 Mimante/.github/workflows/ci-cd.yml create mode 100644 Mimante/.gitignore create mode 100644 Mimante/Data/.gitkeep create mode 100644 Mimante/Makefile delete mode 100644 Mimante/Pages/Browser.razor create mode 100644 Mimante/Pages/Health.razor create mode 100644 Mimante/Services/DatabaseService.cs create mode 100644 Mimante/docker-compose.dev.yml create mode 100644 Mimante/wwwroot/css/app-wpf.css diff --git a/Mimante/.dockerignore b/Mimante/.dockerignore index 06ddeba..5521130 100644 --- a/Mimante/.dockerignore +++ b/Mimante/.dockerignore @@ -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 diff --git a/Mimante/.env.example b/Mimante/.env.example new file mode 100644 index 0000000..2f50f19 --- /dev/null +++ b/Mimante/.env.example @@ -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 diff --git a/Mimante/.gitea/workflows/deploy.yml b/Mimante/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..32cdd64 --- /dev/null +++ b/Mimante/.gitea/workflows/deploy.yml @@ -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 diff --git a/Mimante/.github/workflows/ci-cd.yml b/Mimante/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..d2160fb --- /dev/null +++ b/Mimante/.github/workflows/ci-cd.yml @@ -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 diff --git a/Mimante/.gitignore b/Mimante/.gitignore new file mode 100644 index 0000000..842531d --- /dev/null +++ b/Mimante/.gitignore @@ -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 diff --git a/Mimante/AutoBidder.csproj b/Mimante/AutoBidder.csproj index a814d21..9e7c6aa 100644 --- a/Mimante/AutoBidder.csproj +++ b/Mimante/AutoBidder.csproj @@ -59,4 +59,13 @@ + + + + + + + + + diff --git a/Mimante/Data/.gitkeep b/Mimante/Data/.gitkeep new file mode 100644 index 0000000..2272f38 --- /dev/null +++ b/Mimante/Data/.gitkeep @@ -0,0 +1 @@ +# This file ensures the data directory is tracked by git diff --git a/Mimante/Dockerfile b/Mimante/Dockerfile index d861001..45eba8e 100644 --- a/Mimante/Dockerfile +++ b/Mimante/Dockerfile @@ -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"] diff --git a/Mimante/Makefile b/Mimante/Makefile new file mode 100644 index 0000000..c208701 --- /dev/null +++ b/Mimante/Makefile @@ -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)" diff --git a/Mimante/Models/AuctionInfo.cs b/Mimante/Models/AuctionInfo.cs index 9497ffb..6695077 100644 --- a/Mimante/Models/AuctionInfo.cs +++ b/Mimante/Models/AuctionInfo.cs @@ -115,6 +115,12 @@ namespace AutoBidder.Models /// [JsonIgnore] public ProductValue? CalculatedValue { get; set; } + + /// + /// Ultimo stato ricevuto dal monitor per questa asta + /// + [JsonIgnore] + public AuctionState? LastState { get; set; } /// /// Aggiunge una voce al log dell'asta con limite automatico di righe diff --git a/Mimante/Pages/Browser.razor b/Mimante/Pages/Browser.razor deleted file mode 100644 index 3527aba..0000000 --- a/Mimante/Pages/Browser.razor +++ /dev/null @@ -1,292 +0,0 @@ -@page "/browser" -@inject IJSRuntime JSRuntime - -Browser - AutoBidder - -
-
-
- -

Browser Integrato

-
-
- - - -
-
- -
-
- - - - - - -
-
- - @if (!string.IsNullOrEmpty(errorMessage)) - { -
-
- -
- Limitazione Browser: @errorMessage -
-
-
- } - - @if (!string.IsNullOrEmpty(extractedCookie)) - { -
-
-
- -
- Cookie Estratto: -
- -
-
-
-
- - - Vai a Impostazioni - -
-
-
- } - -
- -
- -
-
- -
-
?? Come Usare il Browser Integrato
-
    -
  1. Clicca su "Apri Bidoo" per caricare il sito
  2. -
  3. Effettua il login con le tue credenziali Bidoo
  4. -
  5. Clicca su "Estrai Cookie" per recuperare il cookie di sessione
  6. -
  7. Il cookie verrà copiato automaticamente negli appunti
  8. -
  9. Vai alla pagina Impostazioni e incollalo nella sezione "Sessione Bidoo"
  10. -
-
- - - Nota: 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. - -
-
-
-
- -
-
-
Metodo Alternativo (Consigliato)
-
-
-

Se l'iframe non funziona correttamente, usa questo metodo:

-
-
-
- -
-
-
Apri Bidoo in una Nuova Scheda
- - https://it.bidoo.com - -
-
- -
-
- -
-
-
Estrai il Cookie Manualmente
-

Usa F12 ? Application/Storage ? Cookies ? bidoo.com ? Copia __stattrb

-
-
- -
-
- -
-
-
Incolla nelle Impostazioni
- - Vai alle Impostazioni - -
-
-
-
-
-
- - - -@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("canGoBack"); - } - catch - { - errorMessage = "Navigazione indietro non disponibile"; - } - } - - private async Task NavigateForward() - { - try - { - await JSRuntime.InvokeVoidAsync("navigateForward"); - canGoForward = await JSRuntime.InvokeAsync("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("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!"); - } - } -} diff --git a/Mimante/Pages/FreeBids.razor b/Mimante/Pages/FreeBids.razor index 7a3f89e..9e7b5d8 100644 --- a/Mimante/Pages/FreeBids.razor +++ b/Mimante/Pages/FreeBids.razor @@ -1,406 +1,53 @@ @page "/freebids" -@inject AuctionMonitor AuctionMonitor -@inject IJSRuntime JSRuntime Puntate Gratuite - AutoBidder
-
-
- -

Puntate Gratuite

-
-
- - -
+
+ +

Puntate Gratuite

- -
-
- + +
+
+
-
?? Funzionalità in Sviluppo
-

- Il sistema di raccolta automatica delle puntate gratuite è attualmente in fase di sviluppo. +

Funzionalita in Sviluppo

+

+ Il sistema di gestione delle puntate gratuite e attualmente in fase di sviluppo e sara disponibile in una prossima versione.

-

- Prossimamente: Rilevamento automatico delle aste con puntate gratuite, - partecipazione automatica e statistiche dettagliate. -

-
-
-
- - -
-
-
-
- -

@totalFreeBids

-

Puntate Gratuite Oggi

-
-
-
-
-
-
- -

@usedFreeBids

-

Puntate Utilizzate

-
-
-
-
-
-
- -

@pendingFreeBids

-

In Attesa

-
-
-
-
-
-
- -

@totalWins

-

Aste Vinte

-
-
-
-
- - -
-
-
Aste con Puntate Gratuite Disponibili
-
-
-
- - - - - - - - - - - - - @if (freeBidsAuctions.Count == 0) - { - - - - } - else - { - @foreach (var auction in freeBidsAuctions) - { - - - - - - - - - } - } - -
Prodotto Puntate Gratuite Scadenza Prezzo Attuale Stato Azioni
-
- -
Nessuna asta con puntate gratuite disponibile
-

Le aste appariranno automaticamente quando disponibili

-
-
@auction.ProductName - - @auction.FreeBidsAvailable - - @auction.ExpiryTime.ToString("dd/MM/yyyy HH:mm")€@auction.CurrentPrice.ToString("F2") - - @auction.Status - - -
- - -
-
-
-
-
- - -
-
-
Configurazione Puntate Gratuite
-
-
-
-
-
- - -
- - Rileva e raccogli automaticamente le puntate gratuite disponibili - -
- -
-
- - -
- - Usa automaticamente le puntate gratuite sulle aste selezionate - -
- -
- - - - Usa puntate gratuite solo su aste sotto questo prezzo - -
- -
- - - - Tempo minimo prima della scadenza per usare puntate gratuite - -
-
- - - - Disponibile nella prossima versione - -
-
- - -
-
-
Come Funzioneranno le Puntate Gratuite
-
-
-
-
-
Rilevamento Automatico
-

- AutoBidder scansionerà continuamente Bidoo.com per identificare le aste che offrono puntate gratuite. -

- -
Raccolta Puntate
-

- Le puntate gratuite verranno raccolte automaticamente non appena disponibili, prima che scadano. -

- -
Utilizzo Strategico
-

- Le puntate verranno utilizzate secondo le tue preferenze configurate (prezzo max, tempo rimanente, ecc.). -

-
-
-
Vantaggi
-
    -
  • ? Zero costo per le puntate gratuite
  • -
  • ? Maggiori opportunità di vincita
  • -
  • ? Nessun rischio economico
  • -
  • ? Gestione completamente automatica
  • -
- -
Note Importanti
-
    -
  • ?? Le puntate gratuite hanno scadenza limitata
  • -
  • ?? Disponibilità limitata per prodotto/utente
  • -
  • ?? Vincere comunque richiede pagamento prodotto
  • -
-
-
- -
-
- -
- Suggerimento: Combina le puntate gratuite con la strategia di bidding normale - per massimizzare le tue possibilità di vincita minimizzando i costi. -
+
+
Funzionalita Previste:
+
    +
  • Rilevamento Automatico: Scansione continua delle aste con puntate gratuite disponibili
  • +
  • Raccolta Automatica: Acquisizione automatica delle puntate gratuite prima della scadenza
  • +
  • Utilizzo Strategico: Uso intelligente delle puntate secondo criteri configurabili
  • +
  • Statistiche Dettagliate: Tracciamento completo di utilizzo, vincite e risparmi
  • +
  • Notifiche: Avvisi in tempo reale per nuove opportunita
  • +
+
+ + Nota: 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.
- -@if (showInfoModal) -{ - -} - - -@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 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(); - } - - 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"; - } -} diff --git a/Mimante/Pages/Health.razor b/Mimante/Pages/Health.razor new file mode 100644 index 0000000..bed29e2 --- /dev/null +++ b/Mimante/Pages/Health.razor @@ -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 diff --git a/Mimante/Pages/Index.razor b/Mimante/Pages/Index.razor index f38476e..df24b51 100644 --- a/Mimante/Pages/Index.razor +++ b/Mimante/Pages/Index.razor @@ -48,8 +48,10 @@ Prezzo Timer Ultimo - Reset Click + Totale + Risparmio + OK? Azioni @@ -68,8 +70,10 @@ @GetPriceDisplay(auction) @GetTimerDisplay(auction) @GetLastBidder(auction) - @auction.ResetCount @GetMyBidsCount(auction) + @GetTotalCostDisplay(auction) + @GetSavingsDisplay(auction) + @GetIsWorthItIcon(auction)
@if (auction.IsActive && !auction.IsPaused) @@ -103,23 +107,42 @@ }
+ +
+
+

Log Globale

+ +
+
+ @if (globalLog.Count == 0) + { +
Nessun log ancora...
+ } + else + { + @foreach (var logEntry in globalLog.TakeLast(100)) + { +
@logEntry
+ } + } +
+
+ + @if (selectedAuction != null) { -
-
-

@selectedAuction.Name

- - @GetStatusIcon(selectedAuction) @GetStatusText(selectedAuction) - -
+
+

@selectedAuction.Name

ID: @selectedAuction.AuctionId

-
+
-
@@ -127,160 +150,150 @@
- - + +
- +
- - + +
- - + +
-
+
+
+ + +
+
+ + +
+
+ +
-
- -
-

Log Asta

-
- @if (GetAuctionLog(selectedAuction).Any()) - { - @foreach (var logEntry in GetAuctionLog(selectedAuction)) - { -
@logEntry
- } - } - else - { -
Nessun log disponibile
- } -
-
- -
-

Partecipanti (@selectedAuction.BidderStats.Count)

- @if (selectedAuction.BidderStats.Count == 0) + + @* ?? NUOVO: Sezione Valore Prodotto *@ + @if (selectedAuction.CalculatedValue != null) { -

Nessun partecipante ancora

- } - else - { -
- - - - - - - - - - @foreach (var bidder in selectedAuction.BidderStats.Values.OrderByDescending(b => b.BidCount)) - { - - - - - - } - -
Utente Puntate Ultima
@bidder.Username@bidder.BidCount@bidder.LastBidTimeDisplay
+
+
Valore Prodotto
+ +
+
+
+
+ Prezzo Compra Subito + @GetBuyNowPriceDisplay(selectedAuction) +
+
+
+
+
+
+ Costo Totale + @GetTotalCostDisplay(selectedAuction) +
+
+
+
+
+
+ Risparmio + @GetSavingsDisplay(selectedAuction) +
+
+
+
+
+
+ Conveniente? + + @GetIsWorthItIcon(selectedAuction) + +
+
+
+ + @if (!string.IsNullOrEmpty(selectedAuction.CalculatedValue.Summary)) + { +
+ @selectedAuction.CalculatedValue.Summary +
+ } }
} else { -
-
- -

Seleziona un'asta

-

Clicca su un'asta dalla lista per visualizzare i dettagli

+
+
+ +

Seleziona un'asta per i dettagli

}
-
-
-

Log Globale

- -
-
- @if (globalLog.Count == 0) - { -
Nessun log ancora...
- } - else - { - @foreach (var logEntry in globalLog.TakeLast(100)) - { -
@logEntry
- } - } -
-
-
- - -@if (showAddDialog) -{ - +
Limiti Log
@@ -161,26 +280,35 @@ - + + + Numero massimo di righe nel log principale +
- + + + Numero massimo di righe per ogni asta +
- + + + Numero massimo di puntate da mantenere (0 = illimitate) +
@@ -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; - } @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 diff --git a/Mimante/Pages/Statistics.razor b/Mimante/Pages/Statistics.razor index bef6c9e..2da3477 100644 --- a/Mimante/Pages/Statistics.razor +++ b/Mimante/Pages/Statistics.razor @@ -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(); + 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(); + errorMessage = "Nessuna statistica disponibile. Il database potrebbe non essere inizializzato."; + } + } + catch (TaskCanceledException) + { + errorMessage = "Timeout durante caricamento statistiche. Riprova più tardi."; + stats = new List(); } catch (Exception ex) { errorMessage = $"Si è verificato un errore: {ex.Message}"; stats = new List(); - Console.WriteLine($"[ERROR] Statistics page: {ex}"); + Console.WriteLine($"[ERROR] Statistics RefreshStats: {ex}"); + Console.WriteLine($"[ERROR] Stack trace: {ex.StackTrace}"); } finally { diff --git a/Mimante/Pages/_Host.cshtml b/Mimante/Pages/_Host.cshtml index eb2eace..06ba84f 100644 --- a/Mimante/Pages/_Host.cshtml +++ b/Mimante/Pages/_Host.cshtml @@ -11,9 +11,8 @@ - + - @@ -27,11 +26,10 @@ Si è verificato un errore non gestito. Consultare la console del browser per ulteriori informazioni. Ricarica - ?? + ?
- diff --git a/Mimante/Pages/_Layout.cshtml b/Mimante/Pages/_Layout.cshtml index ce0caf9..bfcd143 100644 --- a/Mimante/Pages/_Layout.cshtml +++ b/Mimante/Pages/_Layout.cshtml @@ -9,7 +9,6 @@ - @@ -23,7 +22,7 @@ Si è verificato un errore non gestito. Consultare la console del browser per ulteriori informazioni. Ricarica - ?? + ?
diff --git a/Mimante/Program.cs b/Mimante/Program.cs index 3014646..34440b7 100644 --- a/Mimante/Program.cs +++ b/Mimante/Program.cs @@ -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(); builder.Services.AddScoped(sp => { var ctx = sp.GetRequiredService(); @@ -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(); + + 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(); - 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 diff --git a/Mimante/Services/AuctionMonitor.cs b/Mimante/Services/AuctionMonitor.cs index 34003b2..7d82024 100644 --- a/Mimante/Services/AuctionMonitor.cs +++ b/Mimante/Services/AuctionMonitor.cs @@ -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; diff --git a/Mimante/Services/DatabaseService.cs b/Mimante/Services/DatabaseService.cs new file mode 100644 index 0000000..4bdf7bf --- /dev/null +++ b/Mimante/Services/DatabaseService.cs @@ -0,0 +1,414 @@ +using Microsoft.Data.Sqlite; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace AutoBidder.Services +{ + /// + /// Servizio per gestione database SQLite con auto-creazione tabelle e migrations + /// + 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}"; + } + + /// + /// Inizializza il database creando le tabelle se necessario + /// + 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); + } + + /// + /// Crea tutte le tabelle necessarie + /// + 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(); + } + + /// + /// Esegue migrations del database + /// + 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); + } + } + } + + /// + /// Ottiene la versione corrente del database + /// + private async Task 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); + } + + /// + /// Imposta la versione del database dopo una migration + /// + 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(); + } + + /// + /// Ottiene una connessione al database + /// + public async Task GetConnectionAsync() + { + var connection = new SqliteConnection(_connectionString); + await connection.OpenAsync(); + return connection; + } + + /// + /// Esegue una query SQL e ritorna il numero di righe affette + /// + public async Task 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(); + } + + /// + /// Esegue una query SQL e ritorna un valore scalare + /// + public async Task 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(); + } + + /// + /// Verifica la salute del database + /// + public async Task 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; + } + } + + /// + /// Ottiene informazioni sul database + /// + public async Task 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; + } + + /// + /// Ottimizza il database (VACUUM) + /// + public async Task OptimizeDatabaseAsync() + { + await using var connection = await GetConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = "VACUUM;"; + await cmd.ExecuteNonQueryAsync(); + } + + /// + /// Backup del database + /// + public async Task 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(); + } + + /// + /// Classe per rappresentare una migration + /// + private class Migration + { + public int Version { get; } + public string Description { get; } + private readonly Func _execute; + + public Migration(int version, string description, Func execute) + { + Version = version; + Description = description; + _execute = execute; + } + + public async Task Execute(SqliteConnection connection) + { + await _execute(connection); + } + } + } + + /// + /// Informazioni sul database + /// + 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]}"; + } + } +} diff --git a/Mimante/Services/StatsService.cs b/Mimante/Services/StatsService.cs index 58cdd3d..f72b0f9 100644 --- a/Mimante/Services/StatsService.cs +++ b/Mimante/Services/StatsService.cs @@ -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> 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(); + } + + 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(); + } + catch (Exception ex) + { + Console.WriteLine($"[StatsService] GetAllStatsAsync failed: {ex.Message}"); + return new List(); + } } } } diff --git a/Mimante/Shared/MainLayout.razor b/Mimante/Shared/MainLayout.razor index ff6e57c..1b7b792 100644 --- a/Mimante/Shared/MainLayout.razor +++ b/Mimante/Shared/MainLayout.razor @@ -24,5 +24,5 @@ Si è verificato un errore non gestito. Consultare la console del browser per ulteriori informazioni. Ricarica - ?? + ?
diff --git a/Mimante/Shared/NavMenu.razor b/Mimante/Shared/NavMenu.razor index 4015bb8..3a7b13a 100644 --- a/Mimante/Shared/NavMenu.razor +++ b/Mimante/Shared/NavMenu.razor @@ -17,11 +17,6 @@ Monitor Aste
-