Compare commits

...
9 Commits
Author SHA1 Message Date
Alby96 0a50ba7df5 Varie migliorie 2026-06-09 18:32:30 +02:00
Alby96 711cc11805 Aggiunti unit test e integrazione CI per AutoBidder
- Creato progetto `Tests/AutoBidder.Tests` con test su servizi, modelli e utility principali
- Aggiornato `.dockerignore` per escludere test e risultati
- Aggiornati workflow GitHub Actions per .NET 10, salvataggio e upload dei risultati test come artifact
- Aggiornata soluzione e csproj per includere/escludere i test correttamente
- I test coprono casi standard ed edge case, senza modifiche funzionali al codice di produzione
2026-04-29 16:09:25 +02:00
Alby96 766d996e44 Archive .NET 10.0 upgrade logs to .github/upgrades/archive/ 2026-03-31 22:34:42 +02:00
Alby96 ed381c92c8 Merge branch 'upgrade-to-NET10' into docker — .NET 10.0 upgrade 2026-03-31 22:31:00 +02:00
Alby96 eb71c34840 Update upgrade tracking files 2026-03-31 22:30:48 +02:00
Alby96 926cbfbbed Update Dockerfile to .NET 10.0 base images 2026-03-31 22:25:07 +02:00
Alby96 0db3c510d4 Upgrade to .NET 10.0 — update framework and packages 2026-03-31 21:18:47 +02:00
Alby96 dd28ed7f58 Commit changes before fixing global.json file(s). 2026-03-31 20:58:14 +02:00
Alby96 94a173d83b Commit changes before fixing global.json file(s). 2026-03-31 18:38:42 +02:00
35 changed files with 4681 additions and 987 deletions
@@ -0,0 +1,4 @@
ID problema,Descrizione,Stato,Gravità,Punti della storia,Percorso progetto,Tipologia di posizione,PERCORSO,Riga,Colonna,ID evento imprevisto,Collegamento alla Guida,Nome dell'assembly,Versione assembly,Chiave pubblica dell'assembly,Frammento
NuGet.0002,È consigliabile eseguire l'aggiornamento del pacchetto NuGet,Active,Potential,1,AutoBidder.csproj,File,AutoBidder.csproj,,,Microsoft.EntityFrameworkCore.Sqlite 8.0.0,,,,,"Microsoft.EntityFrameworkCore.Sqlite, 8.0.0 Raccomandazione: Remove Microsoft.EntityFrameworkCore.Sqlite, and replace with new package Microsoft.EntityFrameworkCore.Sqlite 10.0.5"
NuGet.0002,È consigliabile eseguire l'aggiornamento del pacchetto NuGet,Active,Potential,1,AutoBidder.csproj,File,AutoBidder.csproj,,,Microsoft.AspNetCore.Identity.EntityFrameworkCore 8.0.0,,,,,"Microsoft.AspNetCore.Identity.EntityFrameworkCore, 8.0.0 Raccomandazione: Remove Microsoft.AspNetCore.Identity.EntityFrameworkCore, and replace with new package Microsoft.AspNetCore.Identity.EntityFrameworkCore 10.0.5"
Project.0002,È necessario modificare i framework di destinazione del progetto,Active,Mandatory,1,AutoBidder.csproj,File,AutoBidder.csproj,,,,,,,,Framework di destinazione corrente: net8.0 Framework di destinazione consigliato: net10.0
1 ID problema Descrizione Stato Gravità Punti della storia Percorso progetto Tipologia di posizione PERCORSO Riga Colonna ID evento imprevisto Collegamento alla Guida Nome dell'assembly Versione assembly Chiave pubblica dell'assembly Frammento
2 NuGet.0002 È consigliabile eseguire l'aggiornamento del pacchetto NuGet Active Potential 1 AutoBidder.csproj File AutoBidder.csproj Microsoft.EntityFrameworkCore.Sqlite 8.0.0 Microsoft.EntityFrameworkCore.Sqlite, 8.0.0 Raccomandazione: Remove Microsoft.EntityFrameworkCore.Sqlite, and replace with new package Microsoft.EntityFrameworkCore.Sqlite 10.0.5
3 NuGet.0002 È consigliabile eseguire l'aggiornamento del pacchetto NuGet Active Potential 1 AutoBidder.csproj File AutoBidder.csproj Microsoft.AspNetCore.Identity.EntityFrameworkCore 8.0.0 Microsoft.AspNetCore.Identity.EntityFrameworkCore, 8.0.0 Raccomandazione: Remove Microsoft.AspNetCore.Identity.EntityFrameworkCore, and replace with new package Microsoft.AspNetCore.Identity.EntityFrameworkCore 10.0.5
4 Project.0002 È necessario modificare i framework di destinazione del progetto Active Mandatory 1 AutoBidder.csproj File AutoBidder.csproj Framework di destinazione corrente: net8.0 Framework di destinazione consigliato: net10.0
@@ -0,0 +1,167 @@
{
"settings": {
"components": {
"code": false,
"binaries": false
},
"targetId": "net10.0",
"targetDisplayName": ".NETCoreApp,Version=v10.0"
},
"analysisStartTime": "2026-03-31T16:24:00.9934354Z",
"analysisEndTime": "2026-03-31T16:24:01.3926356Z",
"privacyModeHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2270980",
"stats": {
"summary": {
"projects": 1,
"issues": 2,
"incidents": 3,
"effort": 3
},
"charts": {
"severity": {
"Mandatory": 1,
"Optional": 0,
"Potential": 2,
"Information": 0
},
"category": {
"NuGet": 2,
"Project": 1
}
}
},
"projects": [
{
"path": "AutoBidder.csproj",
"startingProject": true,
"issues": 2,
"storyPoints": 3,
"properties": {
"appName": "AutoBidder",
"projectKind": "AspNetCore",
"frameworks": [
"net8.0"
],
"languages": [
"C#"
],
"tools": [
"MSBuild"
],
"isSdkStyle": true,
"numberOfFiles": 76,
"numberOfCodeFiles": 50,
"linesTotal": 24601,
"linesOfCode": 14361,
"totalApiScanned": 0,
"minLinesOfCodeToChange": 0,
"maxLinesOfCodeToChange": 0
},
"ruleInstances": [
{
"incidentId": "3d50aa32-1a3a-4f89-8e30-cc70a68af57e",
"ruleId": "NuGet.0002",
"projectPath": "AutoBidder.csproj",
"state": "Active",
"location": {
"snippetModel": {
"unrestricted": "Microsoft.EntityFrameworkCore.Sqlite, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.EntityFrameworkCore.Sqlite, and replace with new package Microsoft.EntityFrameworkCore.Sqlite 10.0.5",
"protected": "Microsoft.EntityFrameworkCore.Sqlite, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.EntityFrameworkCore.Sqlite, and replace with new package Microsoft.EntityFrameworkCore.Sqlite 10.0.5"
},
"kind": "File",
"path": "AutoBidder.csproj",
"snippet": "Microsoft.EntityFrameworkCore.Sqlite, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.EntityFrameworkCore.Sqlite, and replace with new package Microsoft.EntityFrameworkCore.Sqlite 10.0.5",
"protectedSnippet": "Microsoft.EntityFrameworkCore.Sqlite, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.EntityFrameworkCore.Sqlite, and replace with new package Microsoft.EntityFrameworkCore.Sqlite 10.0.5",
"label": "Microsoft.EntityFrameworkCore.Sqlite 8.0.0",
"properties": {
"PackageId": "Microsoft.EntityFrameworkCore.Sqlite",
"PackageVersion": "8.0.0",
"PackageNewVersion": "10.0.5",
"PackageReplacements": null
}
}
},
{
"incidentId": "1876bbae-2d8e-419c-aedc-f4b0d63ad5ee",
"ruleId": "NuGet.0002",
"projectPath": "AutoBidder.csproj",
"state": "Active",
"location": {
"snippetModel": {
"unrestricted": "Microsoft.AspNetCore.Identity.EntityFrameworkCore, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.AspNetCore.Identity.EntityFrameworkCore, and replace with new package Microsoft.AspNetCore.Identity.EntityFrameworkCore 10.0.5",
"protected": "Microsoft.AspNetCore.Identity.EntityFrameworkCore, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.AspNetCore.Identity.EntityFrameworkCore, and replace with new package Microsoft.AspNetCore.Identity.EntityFrameworkCore 10.0.5"
},
"kind": "File",
"path": "AutoBidder.csproj",
"snippet": "Microsoft.AspNetCore.Identity.EntityFrameworkCore, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.AspNetCore.Identity.EntityFrameworkCore, and replace with new package Microsoft.AspNetCore.Identity.EntityFrameworkCore 10.0.5",
"protectedSnippet": "Microsoft.AspNetCore.Identity.EntityFrameworkCore, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.AspNetCore.Identity.EntityFrameworkCore, and replace with new package Microsoft.AspNetCore.Identity.EntityFrameworkCore 10.0.5",
"label": "Microsoft.AspNetCore.Identity.EntityFrameworkCore 8.0.0",
"properties": {
"PackageId": "Microsoft.AspNetCore.Identity.EntityFrameworkCore",
"PackageVersion": "8.0.0",
"PackageNewVersion": "10.0.5",
"PackageReplacements": null
}
}
},
{
"incidentId": "ad92ba91-4bd4-46fa-8249-f369bb27e7d6",
"ruleId": "Project.0002",
"projectPath": "AutoBidder.csproj",
"state": "Active",
"location": {
"snippetModel": {
"unrestricted": "Framework di destinazione corrente: net8.0\nFramework di destinazione consigliato: net10.0",
"protected": "Framework di destinazione corrente: net8.0\nFramework di destinazione consigliato: net10.0"
},
"kind": "File",
"path": "AutoBidder.csproj",
"snippet": "Framework di destinazione corrente: net8.0\nFramework di destinazione consigliato: net10.0",
"protectedSnippet": "Framework di destinazione corrente: net8.0\nFramework di destinazione consigliato: net10.0",
"properties": {
"CurrentTargetFramework": "net8.0",
"RecommendedTargetFramework": "net10.0"
}
}
}
],
"features": []
}
],
"rules": {
"NuGet.0002": {
"id": "NuGet.0002",
"isFeature": false,
"description": "Per il framework di destinazione selezionato \u00E8 consigliato l\u0027aggiornamento del pacchetto NuGet.\n\n\u00C8 consigliabile che i pacchetti .NET standard abbiano versioni corrispondenti alla versione di .NET a cui \u00E8 destinato il progetto.\n\nAlcuni altri pacchetti sono anche noti per funzionare meglio con i framework di destinazione selezionati.",
"label": "\u00C8 consigliabile eseguire l\u0027aggiornamento del pacchetto NuGet",
"severity": "Potential",
"effort": 1,
"links": [
{
"url": "https://go.microsoft.com/fwlink/?linkid=2262530",
"isCustom": false
}
]
},
"Project.0002": {
"id": "Project.0002",
"isFeature": false,
"description": "Il framework di destinazione del progetto devono essere sostituiti con il nuovo framework di destinazione selezionato per questo aggiornamento.\n\nDurante l\u0027aggiornamento, il framework di destinazione verr\u00E0 modificato in base alla piattaforma corrispondente, se applicabile. In alcuni casi il progetto avr\u00E0 pi\u00F9 framework di destinazione dopo l\u0027aggiornamento se prima usava funzionalit\u00E0 che ora dispongono di piattaforme proprie nelle versioni moderne di .NET Framework (Windows, iOS, Android e cos\u00EC via).",
"label": "\u00C8 necessario modificare i framework di destinazione del progetto",
"severity": "Mandatory",
"effort": 1,
"links": [
{
"title": "Panoramica del trasferimento da .NET Framework a .NET",
"url": "https://go.microsoft.com/fwlink/?linkid=2265227",
"isCustom": false
},
{
"title": "SDK di progetto .NET",
"url": "https://go.microsoft.com/fwlink/?linkid=2265226",
"isCustom": false
}
]
}
}
}
@@ -0,0 +1,136 @@
# Projects and dependencies analysis
This document provides a comprehensive overview of the projects and their dependencies in the context of upgrading to .NETCoreApp,Version=v10.0.
## Table of Contents
- [Executive Summary](#executive-Summary)
- [Highlevel Metrics](#highlevel-metrics)
- [Projects Compatibility](#projects-compatibility)
- [Package Compatibility](#package-compatibility)
- [API Compatibility](#api-compatibility)
- [Aggregate NuGet packages details](#aggregate-nuget-packages-details)
- [Top API Migration Challenges](#top-api-migration-challenges)
- [Technologies and Features](#technologies-and-features)
- [Most Frequent API Issues](#most-frequent-api-issues)
- [Projects Relationship Graph](#projects-relationship-graph)
- [Project Details](#project-details)
- [AutoBidder.csproj](#autobiddercsproj)
## Executive Summary
### Highlevel Metrics
| Metric | Count | Status |
| :--- | :---: | :--- |
| Total Projects | 1 | All require upgrade |
| Total NuGet Packages | 3 | 2 need upgrade |
| Total Code Files | 50 | |
| Total Code Files with Incidents | 1 | |
| Total Lines of Code | 14361 | |
| Total Number of Issues | 3 | |
| Estimated LOC to modify | 0+ | at least 0,0% of codebase |
### Projects Compatibility
| Project | Target Framework | Difficulty | Package Issues | API Issues | Est. LOC Impact | Description |
| :--- | :---: | :---: | :---: | :---: | :---: | :--- |
| [AutoBidder.csproj](#autobiddercsproj) | net8.0 | 🟢 Low | 2 | 0 | | AspNetCore, Sdk Style = True |
### Package Compatibility
| Status | Count | Percentage |
| :--- | :---: | :---: |
| ✅ Compatible | 1 | 33,3% |
| ⚠️ Incompatible | 0 | 0,0% |
| 🔄 Upgrade Recommended | 2 | 66,7% |
| ***Total NuGet Packages*** | ***3*** | ***100%*** |
### API Compatibility
| Category | Count | Impact |
| :--- | :---: | :--- |
| 🔴 Binary Incompatible | 0 | High - Require code changes |
| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing |
| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime |
| ✅ Compatible | 0 | |
| ***Total APIs Analyzed*** | ***0*** | |
## Aggregate NuGet packages details
| Package | Current Version | Suggested Version | Projects | Description |
| :--- | :---: | :---: | :--- | :--- |
| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 8.0.0 | 10.0.5 | [AutoBidder.csproj](#autobiddercsproj) | È consigliabile eseguire l'aggiornamento del pacchetto NuGet |
| Microsoft.EntityFrameworkCore.Sqlite | 8.0.0 | 10.0.5 | [AutoBidder.csproj](#autobiddercsproj) | È consigliabile eseguire l'aggiornamento del pacchetto NuGet |
| Npgsql.EntityFrameworkCore.PostgreSQL | 8.0.0 | | [AutoBidder.csproj](#autobiddercsproj) | ✅Compatible |
## Top API Migration Challenges
### Technologies and Features
| Technology | Issues | Percentage | Migration Path |
| :--- | :---: | :---: | :--- |
### Most Frequent API Issues
| API | Count | Percentage | Category |
| :--- | :---: | :---: | :--- |
## Projects Relationship Graph
Legend:
📦 SDK-style project
⚙️ Classic project
```mermaid
flowchart LR
P1["<b>📦&nbsp;AutoBidder.csproj</b><br/><small>net8.0</small>"]
click P1 "#autobiddercsproj"
```
## Project Details
<a id="autobiddercsproj"></a>
### AutoBidder.csproj
#### Project Info
- **Current Target Framework:** net8.0
- **Proposed Target Framework:** net10.0
- **SDK-style**: True
- **Project Kind:** AspNetCore
- **Dependencies**: 0
- **Dependants**: 0
- **Number of Files**: 76
- **Number of Files with Incidents**: 1
- **Lines of Code**: 14361
- **Estimated LOC to modify**: 0+ (at least 0,0% of the project)
#### Dependency Graph
Legend:
📦 SDK-style project
⚙️ Classic project
```mermaid
flowchart TB
subgraph current["AutoBidder.csproj"]
MAIN["<b>📦&nbsp;AutoBidder.csproj</b><br/><small>net8.0</small>"]
click MAIN "#autobiddercsproj"
end
```
### API Compatibility
| Category | Count | Impact |
| :--- | :---: | :--- |
| 🔴 Binary Incompatible | 0 | High - Require code changes |
| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing |
| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime |
| ✅ Compatible | 0 | |
| ***Total APIs Analyzed*** | ***0*** | |
@@ -0,0 +1,69 @@
## [2026-03-31 20:58] TASK-001: Verify Prerequisites
Status: Complete
- **Verified**:
- .NET 10.0 SDK is installed and available
- No global.json file present, no conflicts with net10.0
- Current branch is `upgrade-to-NET10`
### Outcome
Success - All prerequisites verified successfully.
## [2026-03-31 21:05] TASK-002: Update Target Framework
Status: Complete
- **Verified**: AutoBidder.csproj now contains `<TargetFramework>net10.0</TargetFramework>` at line 4
- **Files Modified**: Mimante/AutoBidder.csproj
- **Code Changes**: Updated TargetFramework from net8.0 to net10.0
### Outcome
Success - Target framework updated to net10.0.
## [2026-03-31 21:10] TASK-003: Update NuGet Packages
Status: Complete
- **Verified**:
- Microsoft.AspNetCore.Identity.EntityFrameworkCore updated to 10.0.5
- Microsoft.EntityFrameworkCore.Sqlite updated to 10.0.5
- Npgsql.EntityFrameworkCore.PostgreSQL updated to 10.0.1 (compatible with EF Core 10)
- dotnet restore completed with no dependency conflicts
- **Files Modified**: Mimante/AutoBidder.csproj
- **Code Changes**: Updated 3 PackageReference versions in AutoBidder.csproj
### Outcome
Success - All NuGet packages updated and restored successfully.
## [2026-03-31 21:14] TASK-004: Build and Fix Compilation Errors
Status: Complete
- **Verified**: Solution builds with 0 errors on first attempt
- **Build Status**: Successful: 0 errors
### Outcome
Success - Solution builds successfully with .NET 10.0 and all updated packages.
## [2026-03-31 21:19] TASK-005: Commit and Final Validation
Status: Complete
- **Verified**:
- All success criteria from Plan §11 met
- AutoBidder.csproj targets net10.0
- All 3 NuGet packages updated to target versions
- Solution builds with 0 errors
- No dependency conflicts
- No security vulnerabilities
- **Commits**: 0db3c51: "Upgrade to .NET 10.0 — update framework and packages"
### Outcome
Success - All changes committed. Upgrade to .NET 10.0 complete.
@@ -0,0 +1,43 @@
# Execution Log — .NET 10.0 Upgrade
## Summary
- **Status**: ? Complete
- **Branch**: `upgrade-to-NET10`
- **Commit**: `0db3c51` — "Upgrade to .NET 10.0 — update framework and packages"
- **Total Tasks**: 5/5 completed, 0 failed, 0 skipped
---
## Task Execution Log
### TASK-001: Verify Prerequisites ?
- .NET 10 SDK: installed and available
- global.json: no file found, no conflicts
- Branch: `upgrade-to-NET10` confirmed
### TASK-002: Update Target Framework ?
- Updated `AutoBidder.csproj` line 4: `net8.0` ? `net10.0`
### TASK-003: Update NuGet Packages ?
- `Microsoft.AspNetCore.Identity.EntityFrameworkCore`: 8.0.0 ? 10.0.5
- `Microsoft.EntityFrameworkCore.Sqlite`: 8.0.0 ? 10.0.5
- `Npgsql.EntityFrameworkCore.PostgreSQL`: 8.0.0 ? 10.0.1
- `dotnet restore` completed with 0 dependency conflicts
### TASK-004: Build and Fix Compilation Errors ?
- Build succeeded on first attempt with 0 errors
- No compilation fixes needed
### TASK-005: Commit and Final Validation ?
- All Plan §11 success criteria verified
- Committed: `0db3c51` on `upgrade-to-NET10`
---
## Files Modified
- `Mimante/AutoBidder.csproj` — TargetFramework + 3 PackageReference versions
- `.github/upgrades/assessment.md` — Created during analysis
- `.github/upgrades/plan.md` — Created during planning
- `.github/upgrades/tasks.md` — Created and tracked during execution
@@ -0,0 +1,318 @@
# .NET 10.0 Upgrade Plan — AutoBidder Solution
## Table of Contents
- [1. Executive Summary](#1-executive-summary)
- [2. Migration Strategy](#2-migration-strategy)
- [3. Detailed Dependency Analysis](#3-detailed-dependency-analysis)
- [4. Project-by-Project Plans](#4-project-by-project-plans)
- [4.1 AutoBidder.csproj](#41-autobiddercsproj)
- [5. Package Update Reference](#5-package-update-reference)
- [6. Breaking Changes Catalog](#6-breaking-changes-catalog)
- [7. Risk Management](#7-risk-management)
- [8. Testing & Validation Strategy](#8-testing--validation-strategy)
- [9. Complexity & Effort Assessment](#9-complexity--effort-assessment)
- [10. Source Control Strategy](#10-source-control-strategy)
- [11. Success Criteria](#11-success-criteria)
---
## 1. Executive Summary
### Scenario
Upgrade the **AutoBidder** Blazor/ASP.NET Core solution from **.NET 8.0** to **.NET 10.0 (LTS)**.
### Scope
| Metric | Value |
|---|---|
| Total Projects | 1 |
| Total NuGet Packages | 3 (2 need upgrade, 1 compatible) |
| Total Lines of Code | 14,361 |
| Total Code Files | 50 |
| API Incompatibilities | 0 |
| Security Vulnerabilities | 0 |
| Estimated LOC Impact | 0+ (minimal) |
### Target State
- All projects targeting `net10.0`
- All NuGet packages updated to .NET 10 compatible versions
- Solution builds with 0 errors
- No security vulnerabilities
### Selected Strategy
**All-At-Once Strategy** — All projects upgraded simultaneously in a single atomic operation.
**Rationale**:
- 1 project (small solution)
- Currently on .NET 8.0 (modern SDK-style)
- Zero project-to-project dependencies
- All packages have known target framework versions available
- No API incompatibilities detected
- No security vulnerabilities
### Complexity Classification
**Simple** — 1 project, dependency depth 0, no high-risk indicators, no vulnerabilities.
**Iteration strategy**: Fast batch (2-3 detail iterations covering all sections).
### Critical Issues
- ?? `Npgsql.EntityFrameworkCore.PostgreSQL` 8.0.0 is marked compatible but has no suggested new version — verify compatibility with EF Core 10.0.5 at build time.
## 2. Migration Strategy
### Approach: All-At-Once
The entire solution is upgraded in a **single coordinated atomic operation**. There are no intermediate states — the project file, all package references, and any required code modifications are applied together.
### Justification
- **1 project** — no dependency ordering complexity
- **0 inter-project dependencies** — no phasing required
- **0 API incompatibilities** — minimal risk of breaking changes
- **Low difficulty** rating from assessment
- **SDK-style project** — clean project file structure
- All packages have target versions or are compatible
### Execution Approach
All operations are performed as a unified upgrade:
1. Update `TargetFramework` in `AutoBidder.csproj` from `net8.0` to `net10.0`
2. Update all NuGet package references to their target versions
3. Restore dependencies
4. Build the solution and fix all compilation errors
5. Verify solution builds with 0 errors
No parallel vs sequential decision is needed — there is only one project.
## 3. Detailed Dependency Analysis
### Dependency Graph
The solution contains a single project with no inter-project dependencies.
```
Level 0 (Foundation):
??? AutoBidder.csproj (AspNetCore, Blazor)
??? Microsoft.AspNetCore.Identity.EntityFrameworkCore 8.0.0
??? Microsoft.EntityFrameworkCore.Sqlite 8.0.0
??? Npgsql.EntityFrameworkCore.PostgreSQL 8.0.0
```
### Project Groupings
Since there is only 1 project with 0 dependencies, all work is performed in a **single atomic operation** with no phasing required.
| Phase | Projects | Description |
|---|---|---|
| Phase 1: Atomic Upgrade | AutoBidder.csproj | Framework + packages upgrade, build, fix errors |
### Critical Path
There is no critical path — the single project is both leaf and root. The entire upgrade is a single step.
### Circular Dependencies
None detected.
## 4. Project-by-Project Plans
### 4.1 AutoBidder.csproj
#### Current State
| Attribute | Value |
|---|---|
| Target Framework | `net8.0` |
| Project Kind | AspNetCore (Blazor) |
| SDK-style | Yes |
| Lines of Code | 14,361 |
| Code Files | 50 (76 total files) |
| NuGet Packages | 3 |
| Project Dependencies | 0 |
| Dependants | 0 |
| Risk Level | ?? Low |
#### Target State
| Attribute | Value |
|---|---|
| Target Framework | `net10.0` |
| Updated Packages | 2 of 3 |
#### Migration Steps
**1. Prerequisites**
- .NET 10 SDK installed ?
- No `global.json` to update ?
**2. Framework Update**
- Update `TargetFramework` property in `AutoBidder.csproj` from `net8.0` to `net10.0`
**3. Package Updates**
| Package | Current Version | Target Version | Reason |
|---|---|---|---|
| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 8.0.0 | 10.0.5 | Upgrade recommended for .NET 10 compatibility |
| Microsoft.EntityFrameworkCore.Sqlite | 8.0.0 | 10.0.5 | Upgrade recommended for .NET 10 compatibility |
| Npgsql.EntityFrameworkCore.PostgreSQL | 8.0.0 | (keep) | ? Compatible — verify at build time with EF Core 10.0.5 |
**4. Expected Breaking Changes**
- No API incompatibilities detected by assessment
- Potential behavioral changes from EF Core 8 ? 10 (see §6 Breaking Changes Catalog)
- Potential ASP.NET Core / Blazor configuration or middleware changes
**5. Code Modifications**
- No code modifications identified by the assessment (0 estimated LOC impact)
- Compilation errors (if any) will be discovered and fixed during the build step
**6. Validation Checklist**
- [ ] `AutoBidder.csproj` targets `net10.0`
- [ ] All package references updated to target versions
- [ ] Solution restores without dependency conflicts
- [ ] Solution builds with 0 errors
- [ ] No security vulnerabilities
## 5. Package Update Reference
### Common Package Updates (affecting AutoBidder.csproj)
| Package | Current | Target | Update Reason |
|---|---|---|---|
| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 8.0.0 | 10.0.5 | Framework compatibility — align with .NET 10 |
| Microsoft.EntityFrameworkCore.Sqlite | 8.0.0 | 10.0.5 | Framework compatibility — align with .NET 10 |
### Compatible Packages (no update required)
| Package | Current Version | Status |
|---|---|---|
| Npgsql.EntityFrameworkCore.PostgreSQL | 8.0.0 | ? Compatible — verify at build time that it works with EF Core 10.0.5 |
### Notes
- All EF Core-related packages should align to the same major version where possible
- `Npgsql.EntityFrameworkCore.PostgreSQL` 8.0.0 is marked compatible by the assessment; however, if build errors occur related to EF Core version mismatch, investigate whether a newer Npgsql version is required
## 6. Breaking Changes Catalog
The assessment detected **0 API incompatibilities**. However, the following areas should be monitored during the build and test phases:
### Framework Breaking Changes (.NET 8 ? .NET 10)
| Area | Risk | Description |
|---|---|---|
| ASP.NET Core / Blazor | Low | Possible middleware, routing, or configuration changes between .NET 8 and .NET 10 |
| Entity Framework Core 8 ? 10 | Low | Possible behavioral changes in query translation, migrations, or model building |
| Base Class Library (BCL) | Low | Minor API behavioral changes possible |
### Package-Specific Considerations
| Package | Consideration |
|---|---|
| Npgsql.EntityFrameworkCore.PostgreSQL 8.0.0 | Verify compatibility with EF Core 10.0.5 — if incompatible, investigate newer Npgsql version |
| Microsoft.AspNetCore.Identity.EntityFrameworkCore 10.0.5 | Identity API changes possible between major versions |
### Discovery Approach
Since no specific breaking changes were identified, the primary discovery mechanism is:
1. Build the solution after all updates
2. Analyze compilation errors
3. Fix identified issues referencing .NET 10 migration documentation
## 7. Risk Management
### Risk Assessment
| Project | Risk Level | Description | Mitigation |
|---|---|---|---|
| AutoBidder.csproj | ?? Low | Single project, 0 API issues, 2 straightforward package updates | Standard upgrade process; rollback via Git if needed |
### Specific Risks
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Npgsql EF Core compatibility | Low | Medium | If build fails due to EF Core version mismatch, upgrade Npgsql to a .NET 10-compatible version |
| Undiscovered breaking changes in Blazor/ASP.NET Core | Low | Low | Fix compilation errors during build step; consult .NET 10 migration guide |
| EF Core migration compatibility | Low | Medium | Verify existing database migrations still apply correctly with EF Core 10 |
### Contingency Plans
- **Build failures from Npgsql**: Search for a newer version of `Npgsql.EntityFrameworkCore.PostgreSQL` compatible with EF Core 10. If none exists, evaluate alternative PostgreSQL providers.
- **Unexpected compilation errors**: Use .NET 10 breaking changes documentation to resolve. The upgrade branch isolates all changes — the `docker` branch remains untouched.
- **Rollback**: Discard the `upgrade-to-NET10` branch and return to `docker` branch.
## 8. Testing & Validation Strategy
### Build Validation
After the atomic upgrade:
- [ ] `dotnet restore` completes without dependency conflicts
- [ ] `dotnet build` completes with 0 errors
- [ ] No new warnings related to deprecated APIs
### Test Execution
No dedicated test projects were identified in the solution. Validation relies on:
1. Successful build with 0 errors
2. No dependency conflicts
3. No security vulnerabilities
> ?? **Recommendation**: If there are test projects not captured in the solution file, or integration/manual tests, execute them after the upgrade to verify runtime behavior — particularly around Entity Framework Core database operations and Blazor component rendering.
## 9. Complexity & Effort Assessment
### Per-Project Complexity
| Project | Complexity | Dependencies | Package Updates | Risk | Notes |
|---|---|---|---|---|---|
| AutoBidder.csproj | ?? Low | 0 projects | 2 packages | Low | Single project, no API issues, straightforward upgrade |
### Overall Assessment
| Factor | Rating |
|---|---|
| Solution Complexity | Low (1 project, 0 dependencies) |
| Package Update Complexity | Low (2 packages with known target versions) |
| Breaking Change Risk | Low (0 API incompatibilities detected) |
| Testing Complexity | Low (build verification only — no test projects) |
| Overall Upgrade Complexity | **Low** |
## 10. Source Control Strategy
### Branching
| Parameter | Value |
|---|---|
| Source branch | `docker` |
| Upgrade branch | `upgrade-to-NET10` |
| Strategy | Single branch for all upgrade changes |
### Commit Approach
**Single commit** for the entire atomic upgrade operation:
- Framework update, package updates, compilation fixes — all in one commit
- Commit message: `Upgrade to .NET 10.0 — update framework and packages`
### Merge Process
1. Verify solution builds with 0 errors on `upgrade-to-NET10`
2. Merge `upgrade-to-NET10` into `docker` (or open a Pull Request for review)
3. Delete `upgrade-to-NET10` branch after successful merge
## 11. Success Criteria
### Technical Criteria
- [ ] `AutoBidder.csproj` targets `net10.0`
- [ ] `Microsoft.AspNetCore.Identity.EntityFrameworkCore` updated to 10.0.5
- [ ] `Microsoft.EntityFrameworkCore.Sqlite` updated to 10.0.5
- [ ] `Npgsql.EntityFrameworkCore.PostgreSQL` compatible (build succeeds)
- [ ] Solution builds with 0 errors
- [ ] No package dependency conflicts
- [ ] No security vulnerabilities
### Quality Criteria
- [ ] Code quality maintained (no degraded patterns)
- [ ] No new compiler warnings from deprecated APIs
### Process Criteria
- [ ] All-At-Once strategy followed (single atomic operation)
- [ ] All changes on `upgrade-to-NET10` branch
- [ ] Single commit for entire upgrade
@@ -0,0 +1,70 @@
# .NET 10.0 Upgrade — Execution Tasks
## Progress Dashboard
| Metric | Value |
|---|---|
| Total Tasks | 5 |
| Completed | 0 |
| In Progress | 0 |
**Progress**: 5/5 tasks complete (100%) ![100%](https://progress-bar.xyz/100)
| Not Started | 5 |
---
## Tasks
### [?] TASK-001: Verify Prerequisites *(Completed: 2026-03-31 20:59)*
**Scope**: Environment validation
**References**: Plan §1, §4.1
**Actions:**
- [?] (1) Verify .NET 10 SDK is installed and available on the machine
- [?] (2) Verify no `global.json` file conflicts with target framework `net10.0`
- [?] (3) Verify current branch is `upgrade-to-NET10`
---
### [?] TASK-002: Update Target Framework *(Completed: 2026-03-31 21:05)*
**Scope**: AutoBidder.csproj
**References**: Plan §2, §4.1
**Actions:**
- [?] (1) Update `TargetFramework` property in `AutoBidder.csproj` from `net8.0` to `net10.0`
- [?] (2) Verify the project file contains `<TargetFramework>net10.0</TargetFramework>`
---
### [?] TASK-003: Update NuGet Packages *(Completed: 2026-03-31 21:11)*
**Scope**: AutoBidder.csproj
**References**: Plan §5
**Actions:**
- [?] (1) Update `Microsoft.AspNetCore.Identity.EntityFrameworkCore` from 8.0.0 to 10.0.5
- [?] (2) Update `Microsoft.EntityFrameworkCore.Sqlite` from 8.0.0 to 10.0.5
- [?] (3) Verify `Npgsql.EntityFrameworkCore.PostgreSQL` 8.0.0 compatibility ? if incompatible with EF Core 10, find and update to a compatible version
- [?] (4) Restore packages and verify no dependency conflicts
---
### [?] TASK-004: Build and Fix Compilation Errors *(Completed: 2026-03-31 21:15)*
**Scope**: AutoBidder.sln
**References**: Plan §6, §8
**Actions:**
- [?] (1) Build the solution
- [?] (2) If build errors exist, analyze and fix all compilation errors
- [?] (3) Rebuild and verify solution builds with 0 errors
---
### [?] TASK-005: Commit and Final Validation *(Completed: 2026-03-31 21:20)*
**Scope**: Repository
**References**: Plan §10, §11
**Actions:**
- [?] (1) Verify all success criteria from Plan ?11 are met
- [?] (2) Stage all changes and commit with message: `Upgrade to .NET 10.0 ? update framework and packages`
- [?] (3) Verify commit succeeded
---
+2 -1
View File
@@ -42,7 +42,8 @@ bld/
**/packages/* **/packages/*
!**/packages/build/ !**/packages/build/
# Test results # Test project and results
Tests/
[Tt]est[Rr]esult*/ [Tt]est[Rr]esult*/
[Bb]uild[Ll]og.* [Bb]uild[Ll]og.*
+11 -4
View File
@@ -11,8 +11,8 @@ on:
workflow_dispatch: # Permette trigger manuale workflow_dispatch: # Permette trigger manuale
env: env:
DOTNET_VERSION: '8.0.x' DOTNET_VERSION: '10.0.x'
REGISTRY: ${{ secrets.GITEA_REGISTRY }} REGISTRY: ${{ secrets.GITEA_REGISTRY }}
jobs: jobs:
# Job 1: Build e Test .NET # Job 1: Build e Test .NET
@@ -45,8 +45,15 @@ jobs:
run: dotnet build --configuration Release --no-restore run: dotnet build --configuration Release --no-restore
- name: Run tests - name: Run tests
run: dotnet test --no-restore --verbosity normal --logger "console;verbosity=detailed" run: dotnet test --no-restore --verbosity normal --logger "console;verbosity=detailed" --results-directory ./test-results --logger "trx;LogFileName=results.trx"
continue-on-error: true
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: ./test-results
retention-days: 30
- name: Publish artifacts - name: Publish artifacts
run: dotnet publish --configuration Release --no-build --output ./publish run: dotnet publish --configuration Release --no-build --output ./publish
+10 -3
View File
@@ -20,7 +20,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: 8.0.x dotnet-version: 10.0.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
@@ -29,8 +29,15 @@ jobs:
run: dotnet build --configuration Release --no-restore run: dotnet build --configuration Release --no-restore
- name: Test - name: Test
run: dotnet test --no-restore --verbosity normal run: dotnet test --no-restore --verbosity normal --results-directory ./test-results --logger "trx;LogFileName=results.trx"
continue-on-error: true
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: ./test-results
retention-days: 30
- name: Publish - name: Publish
run: dotnet publish --configuration Release --no-build --output ./publish run: dotnet publish --configuration Release --no-build --output ./publish
+19 -10
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>AutoBidder</AssemblyName> <AssemblyName>AutoBidder</AssemblyName>
@@ -25,6 +25,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<!-- Exclude test project from main build -->
<Compile Remove="Tests\AutoBidder.Tests\**" />
<Content Remove="Tests\AutoBidder.Tests\**" />
<EmbeddedResource Remove="Tests\AutoBidder.Tests\**" />
<None Remove="Tests\AutoBidder.Tests\**" />
<!-- Exclude WPF files from compilation --> <!-- Exclude WPF files from compilation -->
<Compile Remove=".github\**" /> <Compile Remove=".github\**" />
<Compile Remove=".vscode\**" /> <Compile Remove=".vscode\**" />
@@ -65,9 +71,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -84,6 +90,7 @@
<None Include="Dockerfile" /> <None Include="Dockerfile" />
<None Include=".dockerignore" /> <None Include=".dockerignore" />
<None Include="Properties\PublishProfiles\GiteaRegistry-Versioned.pubxml.user" /> <None Include="Properties\PublishProfiles\GiteaRegistry-Versioned.pubxml.user" />
<None Include="Tests\AutoBidder.Tests\AutoBidder.Tests.csproj" />
</ItemGroup> </ItemGroup>
<!-- ============================================ --> <!-- ============================================ -->
@@ -100,14 +107,14 @@
<Message Importance="high" Text="" /> <Message Importance="high" Text="" />
<Message Importance="high" Text="+-------------------------------------------------------------------+" /> <Message Importance="high" Text="+-------------------------------------------------------------------+" />
<Message Importance="high" Text="¦ POST-BUILD: Pubblicazione su Gitea Container Registry ¦" /> <Message Importance="high" Text=" POST-BUILD: Pubblicazione su Gitea Container Registry " />
<Message Importance="high" Text="+-------------------------------------------------------------------+" /> <Message Importance="high" Text="+-------------------------------------------------------------------+" />
<Message Importance="high" Text="" /> <Message Importance="high" Text="" />
<Message Importance="high" Text="?? Solution Version: $(Version)" /> <Message Importance="high" Text="?? Solution Version: $(Version)" />
<Message Importance="high" Text="?? Local Image: $(LocalImageName):latest" /> <Message Importance="high" Text="?? Local Image: $(LocalImageName):latest" />
<Message Importance="high" Text="??? Target Tags:" /> <Message Importance="high" Text="??? Target Tags:" />
<Message Importance="high" Text=" $(GiteaImageLatest)" /> <Message Importance="high" Text=" $(GiteaImageLatest)" />
<Message Importance="high" Text=" $(GiteaImageVersion)" /> <Message Importance="high" Text=" $(GiteaImageVersion)" />
<Message Importance="high" Text="" /> <Message Importance="high" Text="" />
<Message Importance="high" Text="-------------------------------------------------------------------" /> <Message Importance="high" Text="-------------------------------------------------------------------" />
<Message Importance="high" Text="??? Tagging images..." /> <Message Importance="high" Text="??? Tagging images..." />
@@ -136,15 +143,15 @@
<Message Importance="high" Text="" /> <Message Importance="high" Text="" />
<Message Importance="high" Text="+-------------------------------------------------------------------+" /> <Message Importance="high" Text="+-------------------------------------------------------------------+" />
<Message Importance="high" Text="¦ ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! ¦" /> <Message Importance="high" Text=" ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! " />
<Message Importance="high" Text="+-------------------------------------------------------------------+" /> <Message Importance="high" Text="+-------------------------------------------------------------------+" />
<Message Importance="high" Text="" /> <Message Importance="high" Text="" />
<Message Importance="high" Text="?? Visualizza su Gitea:" /> <Message Importance="high" Text="?? Visualizza su Gitea:" />
<Message Importance="high" Text=" https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder" /> <Message Importance="high" Text=" https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder" />
<Message Importance="high" Text="" /> <Message Importance="high" Text="" />
<Message Importance="high" Text="?? Tag pubblicati:" /> <Message Importance="high" Text="?? Tag pubblicati:" />
<Message Importance="high" Text=" latest (sempre aggiornato all'ultima versione)" /> <Message Importance="high" Text=" latest (sempre aggiornato all'ultima versione)" />
<Message Importance="high" Text=" $(Version) (versione solution corrente)" /> <Message Importance="high" Text=" $(Version) (versione solution corrente)" />
<Message Importance="high" Text="" /> <Message Importance="high" Text="" />
<Message Importance="high" Text="?? Pull command:" /> <Message Importance="high" Text="?? Pull command:" />
<Message Importance="high" Text=" docker pull $(GiteaImageLatest)" /> <Message Importance="high" Text=" docker pull $(GiteaImageLatest)" />
@@ -155,3 +162,5 @@
</Target> </Target>
</Project> </Project>
+28 -1
View File
@@ -1,10 +1,14 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18 # Visual Studio Version 18
VisualStudioVersion = 18.0.11217.181 d18.0 VisualStudioVersion = 18.0.11217.181
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoBidder", "AutoBidder.csproj", "{9BBAEF93-DF66-432C-9349-459E272D6538}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoBidder", "AutoBidder.csproj", "{9BBAEF93-DF66-432C-9349-459E272D6538}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoBidder.Tests", "Tests\AutoBidder.Tests\AutoBidder.Tests.csproj", "{A54E2BBB-8921-428F-A9B4-C042ABD1B584}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -39,10 +43,33 @@ Global
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x64.Build.0 = Release|Any CPU {9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x64.Build.0 = Release|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x86.ActiveCfg = Release|Any CPU {9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x86.ActiveCfg = Release|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x86.Build.0 = Release|Any CPU {9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x86.Build.0 = Release|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|ARM.ActiveCfg = Debug|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|ARM.Build.0 = Debug|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|ARM64.Build.0 = Debug|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|x64.ActiveCfg = Debug|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|x64.Build.0 = Debug|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|x86.ActiveCfg = Debug|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|x86.Build.0 = Debug|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|Any CPU.Build.0 = Release|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|ARM.ActiveCfg = Release|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|ARM.Build.0 = Release|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|ARM64.ActiveCfg = Release|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|ARM64.Build.0 = Release|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|x64.ActiveCfg = Release|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|x64.Build.0 = Release|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|x86.ActiveCfg = Release|Any CPU
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{A54E2BBB-8921-428F-A9B4-C042ABD1B584} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1C55CA56-D270-4D9A-91DA-410BF131E905} SolutionGuid = {1C55CA56-D270-4D9A-91DA-410BF131E905}
EndGlobalSection EndGlobalSection
+4 -4
View File
@@ -1,7 +1,7 @@
# ============================================ # ============================================
# STAGE 1: Build # STAGE 1: Build
# ============================================ # ============================================
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release ARG BUILD_CONFIGURATION=Release
WORKDIR /src WORKDIR /src
@@ -30,7 +30,7 @@ RUN dotnet publish "./AutoBidder.csproj" \
# ============================================ # ============================================
# STAGE 3: Final Runtime # STAGE 3: Final Runtime
# ============================================ # ============================================
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app WORKDIR /app
# Install curl for healthcheck and sqlite3 # Install curl for healthcheck and sqlite3
@@ -71,8 +71,8 @@ HEALTHCHECK --interval=30s --timeout=30s --start-period=90s --retries=5 \
# Labels for metadata # Labels for metadata
LABEL org.opencontainers.image.title="AutoBidder" \ LABEL org.opencontainers.image.title="AutoBidder" \
org.opencontainers.image.description="Sistema automatizzato gestione aste Bidoo - Blazor .NET 8" \ org.opencontainers.image.description="Sistema automatizzato gestione aste Bidoo - Blazor .NET 10" \
org.opencontainers.image.version="1.2.0" \ org.opencontainers.image.version="1.2.0" \
org.opencontainers.image.vendor="Alby96" \ org.opencontainers.image.vendor="Alby96" \
org.opencontainers.image.source="https://gitea.encke-hake.ts.net/Alby96/Mimante" org.opencontainers.image.source="https://gitea.encke-hake.ts.net/Alby96/Mimante"
+136 -102
View File
@@ -1,4 +1,4 @@
@page @page
@model AutoBidder.Pages.Account.LoginModel @model AutoBidder.Pages.Account.LoginModel
@{ @{
Layout = null; Layout = null;
@@ -10,13 +10,28 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Login - AutoBidder</title> <title>Login - AutoBidder</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet" /> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet" />
<style> <style>
* { *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
margin: 0;
padding: 0; :root {
box-sizing: border-box; --md-primary: #6750A4;
--md-on-primary: #FFFFFF;
--md-primary-container: #EADDFF;
--md-on-primary-container: #21005D;
--md-surface: #FEF7FF;
--md-surface-container: #F3EDF7;
--md-surface-container-low: #F7F2FA;
--md-on-surface: #1C1B1F;
--md-on-surface-variant: #49454F;
--md-outline: #79747E;
--md-outline-variant: #CAC4D0;
--md-error: #B3261E;
--md-error-container: #F9DEDC;
--md-on-error-container: #410E0B;
} }
body { body {
@@ -24,143 +39,162 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); background-color: var(--md-surface);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Roboto', system-ui, -apple-system, sans-serif;
color: var(--md-on-surface);
padding: 24px;
} }
.login-card { .login-surface {
background: rgba(255, 255, 255, 0.05); background-color: var(--md-surface-container-low);
backdrop-filter: blur(10px); border-radius: 28px;
border: 1px solid rgba(255, 255, 255, 0.1); padding: 40px 32px;
border-radius: 20px;
padding: 40px;
width: 100%; width: 100%;
max-width: 380px; max-width: 400px;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 6px rgba(0,0,0,.08), 0 4px 16px rgba(103,80,164,.1);
} }
.login-header { .login-hero {
text-align: center; text-align: center;
margin-bottom: 35px; margin-bottom: 32px;
} }
.login-header h1 { .login-icon {
color: #fff; width: 64px;
height: 64px;
background-color: var(--md-primary-container);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 16px;
font-size: 32px;
color: var(--md-on-primary-container);
}
.login-hero h1 {
font-size: 28px; font-size: 28px;
font-weight: 600; font-weight: 400;
margin-bottom: 8px; color: var(--md-on-surface);
margin-bottom: 4px;
} }
.login-header p { .login-hero p {
color: rgba(255, 255, 255, 0.6);
font-size: 14px; font-size: 14px;
color: var(--md-on-surface-variant);
} }
.form-floating { .form-field {
margin-bottom: 20px; position: relative;
margin-bottom: 16px;
} }
.form-floating .form-control { .form-field label {
background: rgba(255, 255, 255, 0.08); display: block;
border: 1px solid rgba(255, 255, 255, 0.15); font-size: 12px;
border-radius: 12px; font-weight: 500;
color: #fff; color: var(--md-on-surface-variant);
height: 55px; margin-bottom: 6px;
padding: 16px;
} }
.form-floating .form-control:focus { .form-field input {
background: rgba(255, 255, 255, 0.12); width: 100%;
border-color: #4f46e5; background-color: var(--md-surface);
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.25); color: var(--md-on-surface);
color: #fff; border: 1px solid var(--md-outline);
border-radius: 4px;
padding: 14px 16px;
font-size: 16px;
font-family: inherit;
outline: none;
transition: border-color .15s, box-shadow .15s;
} }
.form-floating .form-control::placeholder { .form-field input:focus {
color: transparent; border: 2px solid var(--md-primary);
padding: 13px 15px;
box-shadow: none;
} }
.form-floating label { .form-field input::placeholder { color: transparent; }
color: rgba(255, 255, 255, 0.5);
padding: 16px; .form-check-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 24px;
} }
.form-floating .form-control:focus ~ label, .form-check-row input[type="checkbox"] {
.form-floating .form-control:not(:placeholder-shown) ~ label { width: 18px;
color: rgba(255, 255, 255, 0.7); height: 18px;
accent-color: var(--md-primary);
cursor: pointer;
} }
.form-check { .form-check-row label {
margin-bottom: 25px;
}
.form-check-input {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.3);
}
.form-check-input:checked {
background-color: #4f46e5;
border-color: #4f46e5;
}
.form-check-label {
color: rgba(255, 255, 255, 0.7);
font-size: 14px; font-size: 14px;
color: var(--md-on-surface-variant);
cursor: pointer;
user-select: none;
} }
.btn-login { .btn-login {
width: 100%; width: 100%;
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); background-color: var(--md-primary);
color: var(--md-on-primary);
border: none; border: none;
border-radius: 12px; border-radius: 9999px;
color: #fff; padding: 0 24px;
font-weight: 600; height: 40px;
font-size: 16px; font-size: 14px;
padding: 14px; font-weight: 500;
transition: all 0.3s ease; font-family: inherit;
letter-spacing: .1px;
cursor: pointer;
transition: filter .2s, box-shadow .2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
} }
.btn-login:hover { .btn-login:hover {
transform: translateY(-2px); filter: brightness(0.92);
box-shadow: 0 10px 30px rgba(79, 70, 229, 0.4); box-shadow: 0 2px 6px rgba(103,80,164,.35);
color: #fff;
}
.btn-login:active {
transform: translateY(0);
} }
.alert-error { .alert-error {
background: rgba(239, 68, 68, 0.15); background-color: var(--md-error-container);
border: 1px solid rgba(239, 68, 68, 0.3); color: var(--md-on-error-container);
border-radius: 12px; border-radius: 12px;
color: #fca5a5;
padding: 12px 16px; padding: 12px 16px;
margin-bottom: 20px; margin-bottom: 16px;
font-size: 14px; font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
} }
.login-footer { .login-footer {
text-align: center; text-align: center;
margin-top: 25px; margin-top: 24px;
padding-top: 20px; padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1); border-top: 1px solid var(--md-outline-variant);
} }
.login-footer small { .login-footer small {
color: rgba(255, 255, 255, 0.4);
font-size: 12px; font-size: 12px;
} color: var(--md-on-surface-variant);
.login-footer i {
margin-right: 5px;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="login-card"> <div class="login-surface">
<div class="login-header"> <div class="login-hero">
<div class="login-icon">
<i class="bi bi-lightning-charge-fill"></i>
</div>
<h1>AutoBidder</h1> <h1>AutoBidder</h1>
<p>Sistema Gestione Aste Bidoo</p> <p>Sistema Gestione Aste Bidoo</p>
</div> </div>
@@ -175,25 +209,25 @@
<form method="post"> <form method="post">
@Html.AntiForgeryToken() @Html.AntiForgeryToken()
<div class="form-floating"> <div class="form-field">
<input type="text" class="form-control" id="username" name="Username"
placeholder="Username" value="@Model.Username" required autocomplete="username" />
<label for="username"><i class="bi bi-person"></i> Username</label> <label for="username"><i class="bi bi-person"></i> Username</label>
<input type="text" id="username" name="Username"
placeholder="Username" value="@Model.Username" required autocomplete="username" />
</div> </div>
<div class="form-floating"> <div class="form-field">
<input type="password" class="form-control" id="password" name="Password"
placeholder="Password" required autocomplete="current-password" />
<label for="password"><i class="bi bi-lock"></i> Password</label> <label for="password"><i class="bi bi-lock"></i> Password</label>
<input type="password" id="password" name="Password"
placeholder="Password" required autocomplete="current-password" />
</div> </div>
<div class="form-check"> <div class="form-check-row">
<input class="form-check-input" type="checkbox" id="rememberMe" name="RememberMe" value="true" /> <input type="checkbox" id="rememberMe" name="RememberMe" value="true" />
<label class="form-check-label" for="rememberMe">Ricordami</label> <label for="rememberMe">Ricordami</label>
</div> </div>
<button type="submit" class="btn btn-login"> <button type="submit" class="btn-login">
<i class="bi bi-box-arrow-in-right"></i> Accedi <i class="bi bi-box-arrow-in-right"></i> Accedi
</button> </button>
</form> </form>
@@ -203,4 +237,4 @@
</div> </div>
</div> </div>
</body> </body>
</html> </html>
+1 -1
View File
@@ -10,7 +10,7 @@
<PageTitle>Esplora Aste - AutoBidder</PageTitle> <PageTitle>Esplora Aste - AutoBidder</PageTitle>
<div class="browser-container animate-fade-in p-4"> <div class="browser-container md-page-padded animate-fade-in">
<!-- Header --> <!-- Header -->
<div class="d-flex align-items-center justify-content-between mb-4 flex-wrap gap-3"> <div class="d-flex align-items-center justify-content-between mb-4 flex-wrap gap-3">
<div class="d-flex align-items-center animate-fade-in-down"> <div class="d-flex align-items-center animate-fade-in-down">
+71 -73
View File
@@ -1,4 +1,4 @@
@page "/" @page "/"
@attribute [Microsoft.AspNetCore.Authorization.Authorize] @attribute [Microsoft.AspNetCore.Authorization.Authorize]
@inject AuctionMonitor AuctionMonitor @inject AuctionMonitor AuctionMonitor
@inject AuctionStateService AuctionStateService @inject AuctionStateService AuctionStateService
@@ -8,87 +8,86 @@
<PageTitle>Monitor Aste - AutoBidder</PageTitle> <PageTitle>Monitor Aste - AutoBidder</PageTitle>
<div class="auction-monitor-container"> <div class="auction-monitor-container">
<!-- Toolbar Compatta --> <!-- MD3 Toolbar -->
<div class="toolbar-compact"> <div class="md-toolbar">
<!-- Pulsanti Azioni Massiva (senza conteggi) --> <!-- Bulk action buttons -->
<div class="btn-group-actions"> <div class="d-flex gap-2 align-items-center">
<button class="action-btn success" @onclick="StartAll" title="Avvia tutte le aste"> <button class="md-icon-btn success" @onclick="StartAll" title="Avvia tutte le aste" style="background-color:var(--md-success-container);color:var(--md-on-success-container);">
<i class="bi bi-play-fill"></i> <i class="bi bi-play-fill"></i>
</button> </button>
<button class="action-btn warning" @onclick="PauseAll" title="Metti in pausa tutte le aste"> <button class="md-icon-btn warning" @onclick="PauseAll" title="Metti in pausa tutte le aste" style="background-color:var(--md-warning-container);color:var(--md-on-warning-container);">
<i class="bi bi-pause-fill"></i> <i class="bi bi-pause-fill"></i>
</button> </button>
<button class="action-btn secondary" @onclick="StopAll" title="Ferma tutte le aste"> <button class="md-icon-btn" @onclick="StopAll" title="Ferma tutte le aste" style="background-color:var(--md-surface-container-highest);color:var(--md-on-surface-variant);">
<i class="bi bi-stop-fill"></i> <i class="bi bi-stop-fill"></i>
</button> </button>
</div> </div>
<!-- Indicatori Stato Aste (tutti gli stati) --> <!-- Status pills -->
<div class="status-indicators"> <div class="d-flex gap-2 flex-wrap align-items-center">
<div class="status-pill total" title="Totale aste"> <span class="md-status-pill total" title="Totale aste">
<i class="bi bi-collection"></i> <i class="bi bi-collection"></i>
<span>@(auctions?.Count ?? 0)</span> <span>@(auctions?.Count ?? 0)</span>
</div> </span>
<div class="status-pill active" title="Aste attive"> <span class="md-status-pill active" title="Aste attive">
<i class="bi bi-play-circle-fill"></i> <i class="bi bi-play-circle-fill"></i>
<span>@GetActiveAuctionsCount()</span> <span>@GetActiveAuctionsCount()</span>
</div> </span>
<div class="status-pill paused" title="Aste in pausa"> <span class="md-status-pill paused" title="Aste in pausa">
<i class="bi bi-pause-circle-fill"></i> <i class="bi bi-pause-circle-fill"></i>
<span>@GetPausedAuctionsCount()</span> <span>@GetPausedAuctionsCount()</span>
</div> </span>
<div class="status-pill stopped" title="Aste fermate"> <span class="md-status-pill stopped" title="Aste fermate">
<i class="bi bi-stop-circle-fill"></i> <i class="bi bi-stop-circle-fill"></i>
<span>@GetStoppedAuctionsCount()</span> <span>@GetStoppedAuctionsCount()</span>
</div> </span>
<div class="status-pill won" title="Aste vinte"> <span class="md-status-pill won" title="Aste vinte">
<i class="bi bi-trophy-fill"></i> <i class="bi bi-trophy-fill"></i>
<span>@GetWonAuctionsCount()</span> <span>@GetWonAuctionsCount()</span>
</div> </span>
<div class="status-pill lost" title="Aste perse"> <span class="md-status-pill lost" title="Aste perse">
<i class="bi bi-x-circle-fill"></i> <i class="bi bi-x-circle-fill"></i>
<span>@GetLostAuctionsCount()</span> <span>@GetLostAuctionsCount()</span>
</div> </span>
</div> </div>
<!-- Pulsanti Gestione --> <!-- Management buttons -->
<div class="btn-group-manage"> <div class="d-flex gap-1 align-items-center ms-auto flex-wrap">
<button class="manage-btn primary" @onclick="ShowAddAuctionDialog" title="Aggiungi nuova asta"> <button class="md-icon-btn primary" @onclick="ShowAddAuctionDialog" title="Aggiungi nuova asta">
<i class="bi bi-plus-lg"></i> <i class="bi bi-plus-lg"></i>
</button> </button>
<button class="manage-btn danger" @onclick="RemoveSelectedAuction" disabled="@(selectedAuction == null)" title="Rimuovi selezionata"> <button class="md-icon-btn danger" @onclick="RemoveSelectedAuction" disabled="@(selectedAuction == null)" title="Rimuovi selezionata">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
<div class="manage-separator"></div> <div style="width:1px;height:24px;background:var(--md-outline-variant);margin:0 4px;"></div>
<button class="manage-btn outline-success" @onclick="RemoveActiveAuctions" disabled="@(GetActiveAuctionsCount() == 0)" title="Rimuovi attive"> <button class="md-icon-btn success" @onclick="RemoveActiveAuctions" disabled="@(GetActiveAuctionsCount() == 0)" title="Rimuovi attive">
<i class="bi bi-play-circle"></i> <i class="bi bi-play-circle"></i>
</button> </button>
<button class="manage-btn outline-warning" @onclick="RemovePausedAuctions" disabled="@(GetPausedAuctionsCount() == 0)" title="Rimuovi in pausa"> <button class="md-icon-btn warning" @onclick="RemovePausedAuctions" disabled="@(GetPausedAuctionsCount() == 0)" title="Rimuovi in pausa">
<i class="bi bi-pause-circle"></i> <i class="bi bi-pause-circle"></i>
</button> </button>
<button class="manage-btn outline-secondary" @onclick="RemoveStoppedAuctions" disabled="@(GetStoppedAuctionsCount() == 0)" title="Rimuovi fermate"> <button class="md-icon-btn" @onclick="RemoveStoppedAuctions" disabled="@(GetStoppedAuctionsCount() == 0)" title="Rimuovi fermate">
<i class="bi bi-stop-circle"></i> <i class="bi bi-stop-circle"></i>
</button> </button>
<button class="manage-btn outline-gold" @onclick="RemoveWonAuctions" disabled="@(GetWonAuctionsCount() == 0)" title="Rimuovi vinte"> <button class="md-icon-btn" @onclick="RemoveWonAuctions" disabled="@(GetWonAuctionsCount() == 0)" title="Rimuovi vinte" style="color:var(--md-primary);">
<i class="bi bi-trophy"></i> <i class="bi bi-trophy"></i>
</button> </button>
<button class="manage-btn outline-danger" @onclick="RemoveLostAuctions" disabled="@(GetLostAuctionsCount() == 0)" title="Rimuovi perse"> <button class="md-icon-btn danger" @onclick="RemoveLostAuctions" disabled="@(GetLostAuctionsCount() == 0)" title="Rimuovi perse">
<i class="bi bi-x-circle"></i> <i class="bi bi-x-circle"></i>
</button> </button>
<div class="manage-separator"></div> <div style="width:1px;height:24px;background:var(--md-outline-variant);margin:0 4px;"></div>
<button class="manage-btn danger-fill" @onclick="RemoveAllAuctions" disabled="@((auctions?.Count ?? 0) == 0)" title="Rimuovi TUTTE"> <button class="md-icon-btn danger" @onclick="RemoveAllAuctions" disabled="@((auctions?.Count ?? 0) == 0)" title="Rimuovi TUTTE">
<i class="bi bi-trash-fill"></i> <i class="bi bi-trash-fill"></i>
</button> </button>
</div> </div>
</div> </div>
<!-- Area Principale con Layout a Griglia --> <!-- Main content grid -->
<div class="main-content-area"> <div class="main-content-area">
<!-- Riga Superiore: Aste + Log -->
<div class="top-row" id="topRow"> <div class="top-row" id="topRow">
<!-- Pannello Aste --> <!-- Auctions panel -->
<div class="panel panel-auctions" id="panelAuctions"> <div class="md-panel" id="panelAuctions">
<div class="panel-header"> <div class="md-panel-header">
<span><i class="bi bi-list-check"></i> Aste Monitorate</span> <span><i class="bi bi-list-check"></i> Aste Monitorate</span>
</div> </div>
@if ((auctions?.Count ?? 0) == 0) @if ((auctions?.Count ?? 0) == 0)
@@ -105,7 +104,7 @@
<tr> <tr>
<th class="col-stato sortable-header" @onclick='() => SortAuctionsBy("stato")'>Stato @GetSortIndicator("stato")</th> <th class="col-stato sortable-header" @onclick='() => SortAuctionsBy("stato")'>Stato @GetSortIndicator("stato")</th>
<th class="col-nome sortable-header" @onclick='() => SortAuctionsBy("nome")'>Nome @GetSortIndicator("nome")</th> <th class="col-nome sortable-header" @onclick='() => SortAuctionsBy("nome")'>Nome @GetSortIndicator("nome")</th>
<th class="col-prezzo sortable-header" @onclick='() => SortAuctionsBy("prezzo")'>€ @GetSortIndicator("prezzo")</th> <th class="col-prezzo sortable-header" @onclick='() => SortAuctionsBy("prezzo")'>€ @GetSortIndicator("prezzo")</th>
<th class="col-timer sortable-header" @onclick='() => SortAuctionsBy("timer")'>Timer @GetSortIndicator("timer")</th> <th class="col-timer sortable-header" @onclick='() => SortAuctionsBy("timer")'>Timer @GetSortIndicator("timer")</th>
<th class="col-ultimo">Ultimo</th> <th class="col-ultimo">Ultimo</th>
<th class="col-click sortable-header" @onclick='() => SortAuctionsBy("puntate")'>Punt. @GetSortIndicator("puntate")</th> <th class="col-click sortable-header" @onclick='() => SortAuctionsBy("puntate")'>Punt. @GetSortIndicator("puntate")</th>
@@ -171,18 +170,18 @@
<!-- Splitter Verticale --> <!-- Splitter Verticale -->
<div class="gutter gutter-vertical" id="gutterVertical"></div> <div class="gutter gutter-vertical" id="gutterVertical"></div>
<!-- Pannello Log --> <!-- Log panel -->
<div class="panel panel-log" id="panelLog"> <div class="md-panel" id="panelLog">
<div class="panel-header"> <div class="md-panel-header">
<span><i class="bi bi-terminal"></i> Log</span> <span><i class="bi bi-terminal"></i> Log</span>
<button class="btn btn-xs btn-secondary" @onclick="ClearGlobalLog"> <button class="md-icon-btn" @onclick="ClearGlobalLog" title="Pulisci log">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
</div> </div>
<div class="panel-content log-box" id="globalLogContainer" @ref="globalLogRef"> <div class="md-panel-content md-log-box" id="globalLogContainer" @ref="globalLogRef">
@if (globalLog.Count == 0) @if (globalLog.Count == 0)
{ {
<div class="text-muted"><i class="bi bi-inbox"></i> Nessun log ancora...</div> <div style="color:var(--md-on-surface-variant);"><i class="bi bi-inbox"></i> Nessun log ancora...</div>
} }
else else
{ {
@@ -195,18 +194,17 @@
</div> </div>
</div> </div>
<!-- Splitter Orizzontale -->
<div class="gutter gutter-horizontal" id="gutterHorizontal"></div> <div class="gutter gutter-horizontal" id="gutterHorizontal"></div>
<!-- Riga Inferiore: Dettagli Asta --> <!-- Bottom row: Auction details -->
<div class="bottom-row" id="bottomRow"> <div class="bottom-row" id="bottomRow">
<div class="panel panel-details" id="panelDetails"> <div class="md-panel" id="panelDetails">
@if (selectedAuction != null) @if (selectedAuction != null)
{ {
<div class="auction-details-content"> <div class="auction-details-content">
<div class="details-header"> <div class="md-panel-header">
<i class="bi bi-info-circle-fill"></i> @selectedAuction.Name <span><i class="bi bi-info-circle-fill" style="color:var(--md-primary);"></i> @selectedAuction.Name</span>
<small class="text-muted">(ID: @selectedAuction.AuctionId)</small> <small style="color:var(--md-on-surface-variant);">ID: @selectedAuction.AuctionId</small>
</div> </div>
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
@@ -260,11 +258,11 @@
<input type="number" class="form-control form-control-sm input-narrow" @bind="selectedAuction.BidBeforeDeadlineMs" @bind:after="SaveAuctions" /> <input type="number" class="form-control form-control-sm input-narrow" @bind="selectedAuction.BidBeforeDeadlineMs" @bind:after="SaveAuctions" />
</div> </div>
<div class="setting-item"> <div class="setting-item">
<label><i class="bi bi-currency-euro"></i> Min €</label> <label><i class="bi bi-currency-euro"></i> Min €</label>
<input type="number" step="0.01" class="form-control form-control-sm input-narrow" @bind="selectedAuction.MinPrice" @bind:after="SaveAuctions" /> <input type="number" step="0.01" class="form-control form-control-sm input-narrow" @bind="selectedAuction.MinPrice" @bind:after="SaveAuctions" />
</div> </div>
<div class="setting-item"> <div class="setting-item">
<label><i class="bi bi-currency-euro"></i> Max €</label> <label><i class="bi bi-currency-euro"></i> Max €</label>
<input type="number" step="0.01" class="form-control form-control-sm input-narrow" @bind="selectedAuction.MaxPrice" @bind:after="SaveAuctions" /> <input type="number" step="0.01" class="form-control form-control-sm input-narrow" @bind="selectedAuction.MaxPrice" @bind:after="SaveAuctions" />
</div> </div>
<div class="setting-item"> <div class="setting-item">
@@ -317,7 +315,7 @@
<i class="bi bi-truck"></i> <i class="bi bi-truck"></i>
<div> <div>
<small>Spedizione</small> <small>Spedizione</small>
<strong>€@(selectedAuction.ShippingCost?.ToString("F2") ?? "0.00")</strong> <strong>€@(selectedAuction.ShippingCost?.ToString("F2") ?? "0.00")</strong>
</div> </div>
</div> </div>
</div> </div>
@@ -325,7 +323,7 @@
<div class="calc-item"> <div class="calc-item">
<i class="bi bi-currency-euro"></i> <i class="bi bi-currency-euro"></i>
<span class="label">Prezzo attuale</span> <span class="label">Prezzo attuale</span>
<span class="value">€@selectedAuction.CalculatedValue.CurrentPrice.ToString("F2")</span> <span class="value">€@selectedAuction.CalculatedValue.CurrentPrice.ToString("F2")</span>
</div> </div>
<div class="calc-item"> <div class="calc-item">
<i class="bi bi-hand-index"></i> <i class="bi bi-hand-index"></i>
@@ -340,13 +338,13 @@
<div class="calc-item"> <div class="calc-item">
<i class="bi bi-cash-coin"></i> <i class="bi bi-cash-coin"></i>
<span class="label">Costo puntate</span> <span class="label">Costo puntate</span>
<span class="value">€@selectedAuction.CalculatedValue.MyBidsCost.ToString("F2")</span> <span class="value">€@selectedAuction.CalculatedValue.MyBidsCost.ToString("F2")</span>
</div> </div>
</div> </div>
<div class="totals-compact"> <div class="totals-compact">
<div class="total-item warning"> <div class="total-item warning">
<span>Costo Totale se vinci</span> <span>Costo Totale se vinci</span>
<strong>€@selectedAuction.CalculatedValue.TotalCostIfWin.ToString("F2")</strong> <strong>€@selectedAuction.CalculatedValue.TotalCostIfWin.ToString("F2")</strong>
</div> </div>
<div class="total-item @(selectedAuction.CalculatedValue.Savings > 0 ? "success" : "danger")"> <div class="total-item @(selectedAuction.CalculatedValue.Savings > 0 ? "success" : "danger")">
<span> <span>
@@ -400,7 +398,7 @@
{ {
<tr class="@(bid.IsMyBid ? "my-bid-row" : "")"> <tr class="@(bid.IsMyBid ? "my-bid-row" : "")">
<td>@if (bid.IsMyBid){<strong class="text-success">@bid.Username</strong><span class="badge bg-success ms-1">TU</span>}else{@bid.Username}</td> <td>@if (bid.IsMyBid){<strong class="text-success">@bid.Username</strong><span class="badge bg-success ms-1">TU</span>}else{@bid.Username}</td>
<td class="fw-bold">€@bid.PriceFormatted</td> <td class="fw-bold">€@bid.PriceFormatted</td>
<td class="text-muted small">@bid.TimeFormatted</td> <td class="text-muted small">@bid.TimeFormatted</td>
<td><span class="badge bg-secondary">@bid.BidType</span></td> <td><span class="badge bg-secondary">@bid.BidType</span></td>
</tr> </tr>
@@ -499,8 +497,8 @@
} }
else else
{ {
<div class="details-placeholder"> <div class="details-placeholder" style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:32px;color:var(--md-on-surface-variant);gap:8px;">
<i class="bi bi-arrow-up"></i> <i class="bi bi-arrow-up" style="font-size:2rem;color:var(--md-primary);"></i>
<p>Seleziona un'asta per visualizzare i dettagli</p> <p>Seleziona un'asta per visualizzare i dettagli</p>
</div> </div>
} }
@@ -512,7 +510,7 @@
<!-- Modal Aggiungi Asta --> <!-- Modal Aggiungi Asta -->
@if (showAddDialog) @if (showAddDialog)
{ {
<div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);"> <div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.3);">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
+77 -43
View File
@@ -1,4 +1,4 @@
@page "/settings" @page "/settings"
@attribute [Microsoft.AspNetCore.Authorization.Authorize] @attribute [Microsoft.AspNetCore.Authorization.Authorize]
@inject SessionService SessionService @inject SessionService SessionService
@inject AuctionMonitor AuctionMonitor @inject AuctionMonitor AuctionMonitor
@@ -7,14 +7,14 @@
<PageTitle>Impostazioni - AutoBidder</PageTitle> <PageTitle>Impostazioni - AutoBidder</PageTitle>
<div class="settings-container px-3 px-md-4 py-3"> <div class="settings-container md-page-padded">
<div class="d-flex align-items-center gap-3 mb-3"> <div class="md-toolbar mb-3" style="border-radius:var(--md-shape-medium);border:none;background:transparent;padding-left:0;">
<i class="bi bi-gear-fill text-primary" style="font-size: 2rem;"></i> <i class="bi bi-gear-fill" style="font-size:2rem;color:var(--md-primary);"></i>
<div> <div>
<h2 class="mb-0 fw-bold">Impostazioni</h2> <h2 class="mb-0 md-headline-small">Impostazioni</h2>
<small class="text-muted">Configura sessione, comportamento aste e limiti.</small> <small style="color:var(--md-on-surface-variant);">Configura sessione, comportamento aste e limiti.</small>
</div>
</div> </div>
</div>
<div class="accordion" id="settingsAccordion"> <div class="accordion" id="settingsAccordion">
<!-- SESSIONE BIDOO --> <!-- SESSIONE BIDOO -->
@@ -162,7 +162,7 @@
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-hourglass-split"></i> Intervallo Ticker (ms)</label> <label class="form-label fw-bold"><i class="bi bi-hourglass-split"></i> Intervallo Ticker (ms)</label>
<input type="number" class="form-control" @bind="settings.TickerIntervalMs" min="10" max="500" /> <input type="number" class="form-control" @bind="settings.TickerIntervalMs" min="10" max="500" />
<div class="form-text">Più basso = più preciso ma più CPU. Consigliato: 50-100ms</div> <div class="form-text">Più basso = più preciso ma più CPU. Consigliato: 50-100ms</div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-funnel"></i> Soglia controllo strategie (ms)</label> <label class="form-label fw-bold"><i class="bi bi-funnel"></i> Soglia controllo strategie (ms)</label>
@@ -197,7 +197,7 @@
<div class="form-text">0 = illimitati</div> <div class="form-text">0 = illimitati</div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo minimo (€)</label> <label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo minimo (€)</label>
<div class="input-group"> <div class="input-group">
<input type="number" step="0.01" class="form-control" @bind="settings.DefaultMinPrice" /> <input type="number" step="0.01" class="form-control" @bind="settings.DefaultMinPrice" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMinPrice))" <button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMinPrice))"
@@ -209,7 +209,7 @@
<div class="form-text">0 = punta a qualsiasi prezzo. Il prezzo deve essere ? a questo valore per puntare.</div> <div class="form-text">0 = punta a qualsiasi prezzo. Il prezzo deve essere ? a questo valore per puntare.</div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo massimo (€)</label> <label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo massimo (€)</label>
<div class="input-group"> <div class="input-group">
<input type="number" step="0.01" class="form-control" @bind="settings.DefaultMaxPrice" /> <input type="number" step="0.01" class="form-control" @bind="settings.DefaultMaxPrice" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMaxPrice))" <button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMaxPrice))"
@@ -221,19 +221,19 @@
<div class="form-text">0 = nessun limite. Se il prezzo supera questo valore, SMETTE di puntare.</div> <div class="form-text">0 = nessun limite. Se il prezzo supera questo valore, SMETTE di puntare.</div>
</div> </div>
<div class="col-12"> <div class="col-12">
<label class="form-label fw-bold"><i class="bi bi-database-gear"></i> Priorità limiti nuove aste</label> <label class="form-label fw-bold"><i class="bi bi-database-gear"></i> Priorità limiti nuove aste</label>
<select class="form-select" @bind="settings.NewAuctionLimitsPriority"> <select class="form-select" @bind="settings.NewAuctionLimitsPriority">
<option value="ProductStats">Usa limiti salvati nelle statistiche prodotto</option> <option value="ProductStats">Usa limiti salvati nelle statistiche prodotto</option>
<option value="GlobalDefaults">Usa sempre limiti globali</option> <option value="GlobalDefaults">Usa sempre limiti globali</option>
</select> </select>
<div class="form-text"> <div class="form-text">
Se "Statistiche prodotto", quando aggiungi un'asta di un prodotto già salvato, verranno usati i limiti personalizzati delle statistiche invece di quelli globali. Se "Statistiche prodotto", quando aggiungi un'asta di un prodotto già salvato, verranno usati i limiti personalizzati delle statistiche invece di quelli globali.
</div> </div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-shield-check"></i> Puntate minime da mantenere</label> <label class="form-label fw-bold"><i class="bi bi-shield-check"></i> Puntate minime da mantenere</label>
<input type="number" class="form-control" @bind="settings.MinimumRemainingBids" /> <input type="number" class="form-control" @bind="settings.MinimumRemainingBids" />
<div class="form-text">Questa è un'impostazione globale</div> <div class="form-text">Questa è un'impostazione globale</div>
</div> </div>
</div> </div>
@@ -263,7 +263,7 @@
<div class="alert alert-info border-0 shadow-sm mb-4"> <div class="alert alert-info border-0 shadow-sm mb-4">
<i class="bi bi-info-circle me-2"></i> <i class="bi bi-info-circle me-2"></i>
<strong>Nota:</strong> Le strategie decidono <strong>SE</strong> puntare, non <strong>QUANDO</strong>. <strong>Nota:</strong> Le strategie decidono <strong>SE</strong> puntare, non <strong>QUANDO</strong>.
Il timing è controllato solo dall'impostazione "Anticipo puntata" nelle Impostazioni Predefinite. Il timing è controllato solo dall'impostazione "Anticipo puntata" nelle Impostazioni Predefinite.
</div> </div>
<h6 class="fw-bold mb-3"><i class="bi bi-thermometer-half"></i> Rilevamento Competizione</h6> <h6 class="fw-bold mb-3"><i class="bi bi-thermometer-half"></i> Rilevamento Competizione</h6>
@@ -346,12 +346,12 @@
<input type="checkbox" class="form-check-input" id="probabilistic" @bind="settings.ProbabilisticBiddingEnabled" /> <input type="checkbox" class="form-check-input" id="probabilistic" @bind="settings.ProbabilisticBiddingEnabled" />
<label class="form-check-label" for="probabilistic"> <label class="form-check-label" for="probabilistic">
<strong>Policy probabilistica</strong> <strong>Policy probabilistica</strong>
<div class="form-text">Decide se puntare con probabilità basata su competizione</div> <div class="form-text">Decide se puntare con probabilità basata su competizione</div>
</label> </label>
</div> </div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label">Probabilità base (0-1)</label> <label class="form-label">Probabilità base (0-1)</label>
<input type="number" step="0.1" class="form-control" @bind="settings.BaseBidProbability" /> <input type="number" step="0.1" class="form-control" @bind="settings.BaseBidProbability" />
</div> </div>
</div> </div>
@@ -393,7 +393,7 @@
<select class="form-select" @bind="settings.AggressiveBidderAction"> <select class="form-select" @bind="settings.AggressiveBidderAction">
<option value="Compete">? Continua normalmente (consigliato)</option> <option value="Compete">? Continua normalmente (consigliato)</option>
<option value="Avoid">?? Evita asta (pausa automatica)</option> <option value="Avoid">?? Evita asta (pausa automatica)</option>
<option value="Outbid">?? Punta più aggressivamente</option> <option value="Outbid">?? Punta più aggressivamente</option>
</select> </select>
</div> </div>
</div> </div>
@@ -419,11 +419,11 @@
<input type="number" class="form-control" @bind="settings.MaxBidsPerAuction" /> <input type="number" class="form-control" @bind="settings.MaxBidsPerAuction" />
</div> </div>
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
<label class="form-label">Budget giornaliero (€)</label> <label class="form-label">Budget giornaliero (€)</label>
<input type="number" step="0.01" class="form-control" @bind="settings.DailyBudgetEuro" /> <input type="number" step="0.01" class="form-control" @bind="settings.DailyBudgetEuro" />
</div> </div>
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
<label class="form-label">Costo medio puntata (€)</label> <label class="form-label">Costo medio puntata (€)</label>
<input type="number" step="0.01" class="form-control" @bind="settings.AverageBidCostEuro" /> <input type="number" step="0.01" class="form-control" @bind="settings.AverageBidCostEuro" />
</div> </div>
</div> </div>
@@ -680,6 +680,40 @@
</div> </div>
</div> </div>
<!-- ASPETTO E TEMA -->
<div class="accordion-item">
<h2 class="accordion-header" id="heading-theme">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-theme" aria-expanded="false" aria-controls="collapse-theme">
<i class="bi bi-moon-stars-fill me-2"></i> Aspetto e Tema
</button>
</h2>
<div id="collapse-theme" class="accordion-collapse collapse" aria-labelledby="heading-theme" data-bs-parent="#settingsAccordion">
<div class="accordion-body">
<p class="text-muted small mb-3">Personalizza l'aspetto visivo dell'applicazione. Le modifiche vengono applicate immediatamente.</p>
<div class="md-theme-toggle mb-3">
<div class="md-theme-toggle-label">
<strong><i class="bi bi-moon-fill me-2"></i>Modalità Scura</strong>
<small>Sostituisce il tema chiaro con colori ottimizzati per ambienti con poca luce.</small>
</div>
<div class="form-check form-switch ms-3 mb-0" style="padding-left:3.5rem;">
<input class="form-check-input" type="checkbox" role="switch" id="darkModeToggle"
style="width:3rem;height:1.5rem;cursor:pointer;"
checked="@settings.DarkMode"
@onchange="ToggleDarkMode" />
</div>
</div>
@if (!string.IsNullOrEmpty(themeMessage))
{
<div class="alert alert-success border-0 mt-2 py-2">
<i class="bi bi-check-circle me-2"></i>@themeMessage
</div>
}
</div>
</div>
</div>
<!-- INFORMAZIONI APPLICAZIONE --> <!-- INFORMAZIONI APPLICAZIONE -->
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="heading-info"> <h2 class="accordion-header" id="heading-info">
@@ -759,29 +793,14 @@
max-width: 1100px; max-width: 1100px;
margin: 0 auto; margin: 0 auto;
} }
.accordion-button {
word-break: break-word;
}
.font-monospace { .font-monospace {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: 'Roboto Mono', ui-monospace, monospace;
font-size: 0.925rem; font-size: 0.875rem;
} }
.fade-in { animation: fadeIn 0.25s ease-in; }
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@@keyframes fadeIn { @@keyframes fadeIn {
from { from { opacity: 0; transform: translateY(-8px); }
opacity: 0; to { opacity: 1; transform: translateY(0); }
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
</style> </style>
@@ -804,6 +823,9 @@ private bool applyToAllSuccess = false;
private HashSet<string> applyingSettings = new(); private HashSet<string> applyingSettings = new();
private string? singleSettingMessage = null; private string? singleSettingMessage = null;
// Tema
private string? themeMessage = null;
private AutoBidder.Utilities.AppSettings settings = new(); private AutoBidder.Utilities.AppSettings settings = new();
private System.Threading.Timer? updateTimer; private System.Threading.Timer? updateTimer;
@@ -907,11 +929,11 @@ private System.Threading.Timer? updateTimer;
break; break;
case nameof(settings.DefaultMinPrice): case nameof(settings.DefaultMinPrice):
auction.MinPrice = settings.DefaultMinPrice; auction.MinPrice = settings.DefaultMinPrice;
settingLabel = $"Prezzo minimo (€{settings.DefaultMinPrice:F2})"; settingLabel = $"Prezzo minimo (€{settings.DefaultMinPrice:F2})";
break; break;
case nameof(settings.DefaultMaxPrice): case nameof(settings.DefaultMaxPrice):
auction.MaxPrice = settings.DefaultMaxPrice; auction.MaxPrice = settings.DefaultMaxPrice;
settingLabel = $"Prezzo massimo (€{settings.DefaultMaxPrice:F2})"; settingLabel = $"Prezzo massimo (€{settings.DefaultMaxPrice:F2})";
break; break;
} }
count++; count++;
@@ -1073,7 +1095,19 @@ private System.Threading.Timer? updateTimer;
private void SaveSettings() private void SaveSettings()
{ {
AutoBidder.Utilities.SettingsManager.Save(settings); AutoBidder.Utilities.SettingsManager.Save(settings);
_ = JSRuntime.InvokeVoidAsync("alert", "? Impostazioni salvate con successo!"); _ = JSRuntime.InvokeVoidAsync("alert", " Impostazioni salvate con successo!");
}
private async Task ToggleDarkMode(ChangeEventArgs e)
{
settings.DarkMode = (bool)(e.Value ?? false);
AutoBidder.Utilities.SettingsManager.Save(settings);
await JSRuntime.InvokeVoidAsync("mdTheme.apply", settings.DarkMode);
themeMessage = settings.DarkMode ? "Modalità scura attivata." : "Modalità chiara attivata.";
StateHasChanged();
await Task.Delay(2500);
themeMessage = null;
StateHasChanged();
} }
private void SetRememberState(bool remember) private void SetRememberState(bool remember)
+67 -404
View File
@@ -20,7 +20,7 @@
</div> </div>
@if (StatsService.IsAvailable) @if (StatsService.IsAvailable)
{ {
<button class="action-btn primary" @onclick="RefreshStats" disabled="@isLoading" title="Aggiorna statistiche"> <button class="md-icon-btn primary" @onclick="RefreshStats" disabled="@isLoading" title="Aggiorna statistiche">
@if (isLoading) @if (isLoading)
{ {
<span class="spinner-border spinner-border-sm"></span> <span class="spinner-border spinner-border-sm"></span>
@@ -972,432 +972,95 @@ private bool isBulkOperating = false;
} }
<style> <style>
/* ??? PAGE LAYOUT ??? */ /* Statistics page — MD3 overrides */
.stats-page {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem;
height: 100%;
overflow: hidden;
}
/* ??? TOOLBAR ??? */
.stats-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.4rem 0.6rem;
background: linear-gradient(135deg, rgba(20, 20, 30, 0.8) 0%, rgba(30, 30, 45, 0.8) 100%);
border-radius: var(--radius-md);
border: 1px solid rgba(255, 255, 255, 0.05);
flex-shrink: 0;
}
.stats-toolbar-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
font-weight: 700;
color: var(--text-primary);
}
.stats-toolbar-title i {
font-size: 1.1rem;
color: #818cf8;
}
.action-btn.primary {
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
color: white;
}
.action-btn.primary:hover {
box-shadow: 0 0 10px rgba(99, 102, 241, 0.5);
transform: translateY(-1px);
}
/* ??? ALERT / LOADING ??? */
.stats-alert {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.25);
border-radius: var(--radius-md);
color: #fbbf24;
}
.stats-alert i { font-size: 1.4rem; }
.stats-alert strong { display: block; font-size: 0.8rem; }
.stats-alert small { opacity: 0.7; font-size: 0.72rem; }
.stats-loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
padding: 3rem;
color: var(--text-secondary);
}
.stats-empty { .stats-empty {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 0.5rem; gap: 8px;
padding: 2rem; padding: 32px;
color: var(--text-secondary); color: var(--md-on-surface-variant);
font-size: 0.78rem; font-size: 13px;
} }
.stats-empty i { font-size: 1.8rem; opacity: 0.4; } .stats-empty i { font-size: 2rem; opacity: 0.4; }
/* ??? PANELS ??? */ .stats-panel-products { flex: 1 1 auto; min-height: 200px; overflow: hidden; }
.stats-panel-products { .stats-panel-auctions { flex: 0 0 auto; max-height: 300px; overflow: hidden; }
flex: 1 1 auto;
min-height: 200px;
overflow: hidden;
}
.stats-panel-auctions {
flex: 0 0 auto;
max-height: 300px;
overflow: hidden;
}
/* ??? FILTER BAR (inside panel-header) ??? */
.stats-filter-bar {
display: flex;
align-items: center;
gap: 0.4rem;
margin-left: auto;
}
/* Filter bar */
.stats-filter-bar { display:flex; align-items:center; gap:6px; margin-left:auto; }
.stats-search { .stats-search {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.25rem; gap: 4px;
background: rgba(255, 255, 255, 0.05); background-color: var(--md-surface-container);
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid var(--md-outline-variant);
border-radius: var(--radius-sm); border-radius: var(--md-shape-extra-small);
padding: 0.15rem 0.4rem; padding: 4px 10px;
font-size: 0.7rem; font-size: 13px;
} }
.stats-search i { color: var(--text-secondary); font-size: 0.65rem; } .stats-search i { color: var(--md-on-surface-variant); font-size: 12px; }
.stats-search input { .stats-search input {
background: transparent; background: transparent;
border: none; border: none;
outline: none; outline: none;
color: var(--text-primary); color: var(--md-on-surface);
font-size: 0.7rem; font-size: 13px;
width: 120px; width: 130px;
} font-family: var(--md-font-family);
.stats-search input::placeholder { color: rgba(255,255,255,0.25); }
.stats-search-clear {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 0;
font-size: 0.7rem;
line-height: 1;
} }
.stats-search input::placeholder { color: var(--md-on-surface-variant); opacity: 0.6; }
.stats-search-clear { background:none; border:none; color:var(--md-on-surface-variant); cursor:pointer; padding:0; font-size:14px; }
.stats-select { /* Inline input (compact table cells) */
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-size: 0.7rem;
padding: 0.15rem 0.35rem;
outline: none;
}
.stats-select option { background: var(--bg-card); }
/* ??? TABLE (shared) ??? */
.stats-table {
width: 100%;
border-collapse: collapse;
font-size: 0.78rem;
}
.stats-table thead {
position: sticky;
top: 0;
z-index: 2;
}
.stats-table th {
padding: 0.4rem 0.5rem;
font-size: 0.68rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
color: var(--text-secondary);
background: rgba(255, 255, 255, 0.03);
border-bottom: 1px solid var(--border-color);
white-space: nowrap;
}
.stats-table td {
padding: 0.3rem 0.5rem;
vertical-align: middle;
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
color: var(--text-primary);
}
.stats-table tbody tr {
transition: background 0.12s ease;
}
.stats-table tbody tr:hover {
background: rgba(99, 102, 241, 0.06);
}
.stats-table tbody tr.row-won {
background: rgba(34, 197, 94, 0.05);
}
.stats-table tbody tr.row-won:hover {
background: rgba(34, 197, 94, 0.1);
}
.sortable-header {
cursor: pointer;
user-select: none;
}
.sortable-header:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.06) !important;
}
/* ??? CELL HELPERS ??? */
.cell-name { font-weight: 600; }
.cell-price { font-weight: 600; color: #818cf8; }
.cell-muted { color: var(--text-secondary); font-size: 0.72rem; }
.stats-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.15rem 0.4rem;
border-radius: 10px;
font-size: 0.68rem;
font-weight: 600;
line-height: 1;
}
.badge-success { background: rgba(34, 197, 94, 0.15); color: #4ade80; }
.badge-danger { background: rgba(239, 68, 68, 0.15); color: #f87171; }
.badge-muted { background: rgba(255, 255, 255, 0.05); color: var(--text-secondary); }
.stats-price { font-weight: 600; color: #818cf8; }
/* ??? PRODUCT TABLE INLINE ??? */
.product-table-inline {
table-layout: fixed;
}
.ps-col-name { width: auto; min-width: 180px; }
.ps-col-num { width: 42px; }
.ps-col-price { width: 62px; }
.ps-col-toggle { width: 34px; }
.ps-col-input { width: 72px; }
.ps-col-actions { width: 120px; }
.ps-product-title {
font-weight: 600;
font-size: 0.78rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 260px;
}
.ps-price-stat {
font-size: 0.72rem;
color: var(--text-secondary);
}
.ps-price-stat.ps-price-avg {
font-weight: 600;
color: #818cf8;
}
.ps-wins {
color: #fbbf24;
font-weight: 600;
}
/* ??? INLINE INPUT (DARK) ??? */
.ps-inline-input { .ps-inline-input {
width: 100%; width: 100%;
font-size: 0.7rem; font-size: 12px;
padding: 0.15rem 0.3rem; padding: 3px 6px;
height: 24px; height: 26px;
text-align: center; text-align: center;
border-radius: 4px; border-radius: var(--md-shape-extra-small);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid var(--md-outline);
background: rgba(255, 255, 255, 0.04); background-color: var(--md-surface-container-lowest);
color: var(--text-primary); color: var(--md-on-surface);
outline: none; outline: none;
transition: border-color 0.15s, box-shadow 0.15s; font-family: var(--md-font-family);
transition: border-color .15s;
} }
.ps-inline-input:focus { .ps-inline-input:focus { border: 2px solid var(--md-primary); padding: 2px 5px; }
border-color: rgba(99, 102, 241, 0.5); .ps-inline-input::placeholder { color: var(--md-on-surface-variant); opacity: 0.5; font-style:italic; font-size:11px; }
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.15);
background: rgba(255, 255, 255, 0.06);
}
.ps-inline-input::placeholder {
color: rgba(255, 255, 255, 0.2);
font-style: italic;
font-size: 0.62rem;
}
/* Hide number input spinners */
.ps-inline-input::-webkit-outer-spin-button, .ps-inline-input::-webkit-outer-spin-button,
.ps-inline-input::-webkit-inner-spin-button { .ps-inline-input::-webkit-inner-spin-button { -webkit-appearance:none; margin:0; }
-webkit-appearance: none; .ps-inline-input[type=number] { -moz-appearance:textfield; }
margin: 0;
}
.ps-inline-input[type=number] {
-moz-appearance: textfield;
}
/* ??? ACTION BUTTONS ??? */
.ps-action-group {
display: flex;
gap: 3px;
justify-content: center;
}
.product-row .btn-xs {
padding: 0.12rem 0.3rem;
font-size: 0.68rem;
line-height: 1.2;
border-radius: 4px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.15s ease;
}
.product-row .btn-xs i { font-size: 0.7rem; }
.btn-outline-info {
background: transparent;
border-color: rgba(56, 189, 248, 0.3);
color: #38bdf8;
}
.btn-outline-info:hover { background: rgba(56, 189, 248, 0.15); }
.btn-success {
background: rgba(34, 197, 94, 0.15);
color: #4ade80;
}
.btn-success:hover { background: rgba(34, 197, 94, 0.3); }
.btn-primary {
background: rgba(99, 102, 241, 0.15);
color: #a5b4fc;
}
.btn-primary:hover { background: rgba(99, 102, 241, 0.3); }
.btn-outline-danger {
background: transparent;
border-color: rgba(239, 68, 68, 0.3);
color: #f87171;
}
.btn-outline-danger:hover { background: rgba(239, 68, 68, 0.15); }
.btn-xs:disabled {
opacity: 0.35;
cursor: not-allowed;
}
/* ??? PRODUCT ROW HOVER ??? */
.product-row {
transition: background 0.12s ease;
}
.product-row:hover {
background: rgba(99, 102, 241, 0.04);
}
/* ??? TOGGLE SWITCH ??? */
.ps-switch {
position: relative;
display: inline-block;
width: 28px;
height: 16px;
cursor: pointer;
}
.ps-switch input {
opacity: 0;
width: 0;
height: 0;
}
.ps-switch-slider {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(255, 255, 255, 0.1);
border-radius: 16px;
transition: background 0.2s ease;
}
.ps-switch-slider::before {
content: '';
position: absolute;
width: 12px;
height: 12px;
left: 2px;
bottom: 2px;
background: var(--text-secondary);
border-radius: 50%;
transition: transform 0.2s ease, background 0.2s ease;
}
.ps-switch input:checked + .ps-switch-slider {
background: rgba(34, 197, 94, 0.3);
}
.ps-switch input:checked + .ps-switch-slider::before {
transform: translateX(12px);
background: #4ade80;
}
/* ??? DISABLED INPUT ??? */
.ps-inline-input.ps-input-disabled, .ps-inline-input.ps-input-disabled,
.ps-inline-input:disabled { .ps-inline-input:disabled { opacity: 0.35; cursor: not-allowed; }
opacity: 0.25;
cursor: not-allowed;
}
/* ═══ BULK ACTION BAR ═══ */ /* Column widths */
.ps-bulk-bar { .ps-col-select { width: 30px; }
display: flex; .ps-col-num { width: 44px; }
align-items: center; .ps-col-price { width: 64px; }
gap: 0.35rem; .ps-col-toggle { width: 36px; }
padding: 0.25rem 0.5rem; .ps-col-input { width: 76px; }
background: rgba(255, 255, 255, 0.02); .ps-col-actions { width: 124px; }
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
flex-shrink: 0; .ps-product-title { font-weight:500; font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:260px; color:var(--md-on-surface); }
flex-wrap: wrap; .ps-price-stat { font-size:12px; color:var(--md-on-surface-variant); }
} .ps-price-stat.ps-price-avg { font-weight:600; color:var(--md-primary); }
.ps-bulk-label { .ps-wins { color:var(--md-warning); font-weight:600; }
font-size: 0.62rem;
color: var(--text-secondary); /* Action groups */
margin-right: 0.25rem; .ps-action-group { display:flex; gap:3px; justify-content:center; }
white-space: nowrap;
} /* Toggle switch — MD3 tonal */
.ps-bulk-bar .btn-xs { .ps-switch { position:relative; display:inline-block; width:28px; height:16px; }
padding: 0.12rem 0.4rem; .ps-switch input { opacity:0; width:0; height:0; }
font-size: 0.62rem; .ps-switch-slider { position:absolute; inset:0; background-color:var(--md-outline-variant); border-radius:999px; cursor:pointer; transition:.2s; }
line-height: 1.3; .ps-switch-slider::before { content:""; position:absolute; width:10px; height:10px; left:3px; bottom:3px; background:white; border-radius:50%; transition:.2s; }
border-radius: 4px; .ps-switch input:checked + .ps-switch-slider { background:var(--md-primary); }
border: 1px solid transparent; .ps-switch input:checked + .ps-switch-slider::before { transform:translateX(12px); }
cursor: pointer;
transition: all 0.15s ease; /* Bulk action bar */
white-space: nowrap; .ps-bulk-bar { display:flex; align-items:center; gap:6px; padding:6px 12px; background-color:var(--md-surface-container-low); border-bottom:1px solid var(--md-outline-variant); flex-wrap:wrap; }
} .ps-bulk-label { font-size:12px; color:var(--md-on-surface-variant); margin-right:4px; white-space:nowrap; }
.ps-bulk-bar .btn-xs i { font-size: 0.6rem; margin-right: 0.15rem; }
.ps-bulk-bar .btn-xs:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.btn-outline-toggle {
background: transparent;
border-color: rgba(168, 162, 158, 0.25);
color: var(--text-secondary);
}
.btn-outline-toggle:hover { background: rgba(168, 162, 158, 0.1); }
</style> </style>
+15
View File
@@ -5,8 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="~/" /> <base href="~/" />
<link rel="icon" type="image/x-icon" href="Icon/favicon.ico" /> <link rel="icon" type="image/x-icon" href="Icon/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" /> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet" /> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet" />
<link href="css/material3.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" /> <link href="css/app.css" rel="stylesheet" />
<link href="css/animations.css" rel="stylesheet" /> <link href="css/animations.css" rel="stylesheet" />
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" /> <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
@@ -26,6 +30,17 @@
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
window.mdTheme = {
apply: function (dark) {
if (dark) {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.removeAttribute('data-theme');
}
}
};
</script>
<script src="_framework/blazor.server.js"></script> <script src="_framework/blazor.server.js"></script>
</body> </body>
</html> </html>
+23 -91
View File
@@ -1,103 +1,35 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@inject IJSRuntime JSRuntime
@inject Microsoft.AspNetCore.Components.NavigationManager NavigationManager
<div class="app-container"> <div class="md-app-container">
<aside class="app-sidebar"> <NavMenu />
<NavMenu />
</aside>
<main class="app-main"> <main class="md-app-main">
<article class="app-content"> <article class="md-app-content">
@Body @Body
</article> </article>
</main> </main>
</div> </div>
<div id="blazor-error-ui"> <div id="blazor-error-ui">
<div class="error-content"> <i class="bi bi-exclamation-triangle-fill me-2"></i>
<i class="bi bi-exclamation-triangle-fill"></i> <span>Si è verificato un errore. <a href="" class="reload">Ricarica</a></span>
<span>Si e verificato un errore. <a href="" class="reload">Ricarica</a></span> <a class="ms-3" onclick="document.getElementById('blazor-error-ui').style.display='none'" style="cursor:pointer;color:inherit;">✕</a>
<button class="dismiss-btn" onclick="this.parentElement.parentElement.style.display='none'">×</button>
</div>
</div> </div>
<style> @code {
.app-container { protected override async Task OnAfterRenderAsync(bool firstRender)
display: flex; {
min-height: 100vh; if (firstRender)
background: #0f0f0f; {
} try
{
.app-sidebar { var s = AutoBidder.Utilities.SettingsManager.Load();
width: 260px; await JSRuntime.InvokeVoidAsync("mdTheme.apply", s.DarkMode);
min-width: 260px; }
height: 100vh; catch { /* JS helper may not be available yet; theme defaults to light mode */ }
position: fixed;
left: 0;
top: 0;
z-index: 1000;
}
.app-main {
flex: 1;
margin-left: 260px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.app-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
}
#blazor-error-ui {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
}
#blazor-error-ui .error-content {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.5rem;
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
color: white;
font-weight: 500;
}
#blazor-error-ui .reload {
color: white;
text-decoration: underline;
}
#blazor-error-ui .dismiss-btn {
margin-left: auto;
background: none;
border: none;
color: white;
font-size: 1.5rem;
cursor: pointer;
opacity: 0.8;
}
#blazor-error-ui .dismiss-btn:hover {
opacity: 1;
}
@@media (max-width: 768px) {
.app-sidebar {
width: 100%;
height: auto;
position: relative;
}
.app-main {
margin-left: 0;
} }
} }
</style> }
+61 -250
View File
@@ -1,91 +1,81 @@
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider @inject AuthenticationStateProvider AuthenticationStateProvider
@inject AuctionMonitor AuctionMonitor @inject AuctionMonitor AuctionMonitor
@implements IDisposable @implements IDisposable
<div class="nav-sidebar"> <div class="md-nav-rail">
<div class="nav-header">
<a class="nav-brand" href=""> <!-- Brand icon -->
<div class="brand-icon"> <div class="md-rail-header">
<i class="bi bi-lightning-charge-fill"></i> <a class="md-rail-brand-icon" href="" title="AutoBidder">
</div> <i class="bi bi-lightning-charge-fill"></i>
<span class="brand-text">AutoBidder</span>
</a> </a>
</div> </div>
<nav class="nav-menu"> <!-- Main navigation destinations -->
<div class="nav-section"> <nav class="md-rail-nav">
<NavLink class="nav-menu-item" href="" Match="NavLinkMatch.All"> <NavLink class="md-rail-item" href="" Match="NavLinkMatch.All" title="Monitor Aste">
<i class="bi bi-display"></i> <div class="md-rail-indicator"><i class="bi bi-display"></i></div>
<span>Monitor Aste</span> <span class="md-rail-label">Monitor</span>
</NavLink> </NavLink>
<NavLink class="nav-menu-item" href="browser">
<i class="bi bi-search"></i>
<span>Esplora Aste</span>
</NavLink>
<NavLink class="nav-menu-item" href="statistics">
<i class="bi bi-bar-chart"></i>
<span>Statistiche</span>
</NavLink>
<NavLink class="nav-menu-item" href="settings">
<i class="bi bi-gear"></i>
<span>Impostazioni</span>
</NavLink>
</div>
<div class="nav-footer"> <NavLink class="md-rail-item" href="browser" title="Esplora Aste">
<!-- Info Sessione Utente --> <div class="md-rail-indicator"><i class="bi bi-search"></i></div>
@if (!string.IsNullOrEmpty(sessionUsername)) <span class="md-rail-label">Esplora</span>
{ </NavLink>
<div class="session-stats">
<div class="session-stat"> <NavLink class="md-rail-item" href="statistics" title="Statistiche">
<i class="bi bi-hand-index-thumb-fill"></i> <div class="md-rail-indicator"><i class="bi bi-bar-chart"></i></div>
<div class="stat-content"> <span class="md-rail-label">Statistiche</span>
<span class="stat-label">Puntate</span> </NavLink>
<span class="stat-value @GetBidsClass()">@sessionRemainingBids</span>
</div> <NavLink class="md-rail-item" href="settings" title="Impostazioni">
</div> <div class="md-rail-indicator"><i class="bi bi-gear"></i></div>
<div class="session-stat"> <span class="md-rail-label">Impostazioni</span>
<i class="bi bi-wallet2"></i> </NavLink>
<div class="stat-content">
<span class="stat-label">Credito</span>
<span class="stat-value text-success">€@sessionShopCredit.ToString("F2")</span>
</div>
</div>
</div>
}
<AuthorizeView>
<Authorized>
<div class="user-badge @(string.IsNullOrEmpty(sessionUsername) ? "disconnected" : "connected")">
<i class="bi bi-person-circle"></i>
<span>@(string.IsNullOrEmpty(sessionUsername) ? "Non connesso" : sessionUsername)</span>
</div>
<a href="/Account/Logout" class="nav-menu-item logout-item">
<i class="bi bi-box-arrow-right"></i>
<span>Logout</span>
</a>
</Authorized>
</AuthorizeView>
</div>
</nav> </nav>
<!-- Footer: session stats + user + logout -->
<div class="md-rail-footer">
@if (!string.IsNullOrEmpty(sessionUsername))
{
<div class="md-rail-stat @GetBidsClass()" title="Puntate rimanenti: @sessionRemainingBids">
<i class="bi bi-hand-index-thumb-fill"></i>
<span>@sessionRemainingBids</span>
</div>
<div class="md-rail-stat" style="color:var(--md-success);" title="Credito: €@sessionShopCredit.ToString("F2")">
<i class="bi bi-wallet2"></i>
<span style="font-size:10px;">€@sessionShopCredit.ToString("F0")</span>
</div>
}
<AuthorizeView>
<Authorized>
<div class="md-rail-user @(string.IsNullOrEmpty(sessionUsername) ? "disconnected" : "connected")"
title="@(string.IsNullOrEmpty(sessionUsername) ? "Non connesso" : sessionUsername)">
<i class="bi bi-person-circle"></i>
</div>
<a href="/Account/Logout" class="md-rail-logout" title="Logout">
<div class="md-rail-indicator"><i class="bi bi-box-arrow-right"></i></div>
<span class="md-rail-label">Esci</span>
</a>
</Authorized>
</AuthorizeView>
</div>
</div> </div>
@code { @code {
private string? sessionUsername; private string? sessionUsername;
private int sessionRemainingBids; private int sessionRemainingBids;
private double sessionShopCredit; private double sessionShopCredit;
private System.Threading.Timer? refreshTimer; private System.Threading.Timer? refreshTimer;
protected override void OnInitialized() protected override void OnInitialized()
{ {
LoadSessionInfo(); LoadSessionInfo();
// Refresh ogni 5 secondi
refreshTimer = new System.Threading.Timer(async _ => refreshTimer = new System.Threading.Timer(async _ =>
{ {
LoadSessionInfo(); LoadSessionInfo();
@@ -120,182 +110,3 @@ private System.Threading.Timer? refreshTimer;
refreshTimer?.Dispose(); refreshTimer?.Dispose();
} }
} }
<style>
.nav-sidebar {
display: flex;
flex-direction: column;
height: 100%;
background: linear-gradient(180deg, #1a1d23 0%, #13151a 100%);
border-right: 1px solid rgba(255, 255, 255, 0.06);
}
.nav-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.nav-brand {
display: flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
transition: opacity 0.2s;
}
.nav-brand:hover {
opacity: 0.9;
}
.brand-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border-radius: 10px;
font-size: 1.25rem;
color: white;
}
.brand-text {
font-size: 1.25rem;
font-weight: 700;
color: white;
letter-spacing: -0.02em;
}
.nav-menu {
display: flex;
flex-direction: column;
flex: 1;
padding: 1rem 0.75rem;
overflow-y: auto;
}
.nav-section {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.nav-menu-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 8px;
color: rgba(255, 255, 255, 0.65);
text-decoration: none;
font-size: 0.9375rem;
font-weight: 500;
transition: all 0.15s ease;
}
.nav-menu-item i {
font-size: 1.125rem;
width: 1.5rem;
text-align: center;
}
.nav-menu-item:hover {
background: rgba(255, 255, 255, 0.06);
color: white;
}
.nav-menu-item.active {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%);
color: #a5b4fc;
}
.nav-menu-item.active i {
color: #818cf8;
}
.nav-footer {
margin-top: auto;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.session-stats {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.75rem;
margin-bottom: 0.75rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.session-stat {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.375rem 0;
}
.session-stat i {
font-size: 0.875rem;
width: 1.25rem;
text-align: center;
color: rgba(255, 255, 255, 0.5);
}
.session-stat .stat-content {
display: flex;
justify-content: space-between;
flex: 1;
align-items: center;
}
.session-stat .stat-label {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
}
.session-stat .stat-value {
font-size: 0.875rem;
font-weight: 600;
}
.session-stat .text-success { color: #22c55e; }
.session-stat .text-warning { color: #f59e0b; }
.session-stat .text-danger { color: #ef4444; }
.user-badge {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 8px;
color: rgba(255, 255, 255, 0.5);
font-size: 0.875rem;
}
.user-badge.connected {
border-left: 3px solid #22c55e;
}
.user-badge.disconnected {
border-left: 3px solid #ef4444;
color: rgba(255, 255, 255, 0.4);
}
.user-badge i {
font-size: 1.25rem;
}
.logout-item {
color: rgba(248, 113, 113, 0.8) !important;
}
.logout-item:hover {
background: rgba(248, 113, 113, 0.1) !important;
color: #f87171 !important;
}
</style>
@@ -0,0 +1,285 @@
using AutoBidder.Models;
using AutoBidder.Services;
using Xunit;
namespace AutoBidder.Tests;
public class ApplicationStateServiceTests
{
// === Auctions ===
[Fact]
public void Auctions_Initially_Empty()
{
var service = new ApplicationStateService();
Assert.Empty(service.Auctions);
}
[Fact]
public void AddAuction_AddsToList()
{
var service = new ApplicationStateService();
service.AddAuction(new AuctionInfo { AuctionId = "1", Name = "Test" });
Assert.Single(service.Auctions);
Assert.Equal("1", service.Auctions[0].AuctionId);
}
[Fact]
public void AddAuction_DuplicateId_DoesNotAdd()
{
var service = new ApplicationStateService();
service.AddAuction(new AuctionInfo { AuctionId = "1", Name = "First" });
service.AddAuction(new AuctionInfo { AuctionId = "1", Name = "Second" });
Assert.Single(service.Auctions);
Assert.Equal("First", service.Auctions[0].Name);
}
[Fact]
public void RemoveAuction_ExistingAuction_RemovesFromList()
{
var service = new ApplicationStateService();
var auction = new AuctionInfo { AuctionId = "1" };
service.AddAuction(auction);
service.RemoveAuction(auction);
Assert.Empty(service.Auctions);
}
[Fact]
public void RemoveAuction_SelectedAuction_ClearsSelection()
{
var service = new ApplicationStateService();
var auction = new AuctionInfo { AuctionId = "1" };
service.AddAuction(auction);
service.SetSelectedAuctionDirect(auction);
service.RemoveAuction(auction);
Assert.Null(service.SelectedAuction);
}
[Fact]
public void UpdateAuction_ExistingAuction_ReplacesInList()
{
var service = new ApplicationStateService();
var original = new AuctionInfo { AuctionId = "1", Name = "Original" };
service.AddAuction(original);
var updated = new AuctionInfo { AuctionId = "1", Name = "Updated" };
service.UpdateAuction(updated);
Assert.Single(service.Auctions);
Assert.Equal("Updated", service.Auctions[0].Name);
}
[Fact]
public void UpdateAuction_SelectedAuction_UpdatesSelection()
{
var service = new ApplicationStateService();
var auction = new AuctionInfo { AuctionId = "1", Name = "Original" };
service.AddAuction(auction);
service.SetSelectedAuctionDirect(auction);
var updated = new AuctionInfo { AuctionId = "1", Name = "Updated" };
service.UpdateAuction(updated);
Assert.Equal("Updated", service.SelectedAuction!.Name);
}
[Fact]
public void SetAuctions_ReplacesEntireList()
{
var service = new ApplicationStateService();
service.AddAuction(new AuctionInfo { AuctionId = "old" });
var newList = new List<AuctionInfo>
{
new() { AuctionId = "new1" },
new() { AuctionId = "new2" }
};
service.SetAuctions(newList);
Assert.Equal(2, service.Auctions.Count);
}
[Fact]
public void GetAuctionById_ExistingId_ReturnsAuction()
{
var service = new ApplicationStateService();
service.AddAuction(new AuctionInfo { AuctionId = "42", Name = "Found" });
var result = service.GetAuctionById("42");
Assert.NotNull(result);
Assert.Equal("Found", result!.Name);
}
[Fact]
public void GetAuctionById_NonExistent_ReturnsNull()
{
var service = new ApplicationStateService();
Assert.Null(service.GetAuctionById("missing"));
}
// === SelectedAuction ===
[Fact]
public void SetSelectedAuctionDirect_SetsWithoutEvent()
{
var service = new ApplicationStateService();
var auction = new AuctionInfo { AuctionId = "1" };
service.SetSelectedAuctionDirect(auction);
Assert.Equal("1", service.SelectedAuction!.AuctionId);
}
// === Log ===
[Fact]
public void AddLog_AddsEntry()
{
var service = new ApplicationStateService();
service.AddLog("Test message");
Assert.Single(service.GlobalLog);
Assert.Equal("Test message", service.GlobalLog[0].Message);
}
[Fact]
public void AddLog_TruncatesAtLimit()
{
var service = new ApplicationStateService();
for (int i = 0; i < 600; i++)
{
service.AddLog($"Message {i}");
}
Assert.True(service.GlobalLog.Count <= 500);
}
[Fact]
public void ClearLog_EmptiesLog()
{
var service = new ApplicationStateService();
service.AddLog("Test");
service.AddLog("Test2");
service.ClearLog();
Assert.Empty(service.GlobalLog);
}
// === Manual Bidding ===
[Fact]
public void IsManualBidding_Default_ReturnsFalse()
{
var service = new ApplicationStateService();
Assert.False(service.IsManualBidding("123"));
}
[Fact]
public void StartManualBidding_SetsFlag()
{
var service = new ApplicationStateService();
service.StartManualBidding("123");
Assert.True(service.IsManualBidding("123"));
}
[Fact]
public void StopManualBidding_ClearsFlag()
{
var service = new ApplicationStateService();
service.StartManualBidding("123");
service.StopManualBidding("123");
Assert.False(service.IsManualBidding("123"));
}
// === Monitoring State ===
[Fact]
public void IsMonitoringActive_Default_False()
{
var service = new ApplicationStateService();
Assert.False(service.IsMonitoringActive);
}
[Fact]
public void IsMonitoringActive_SetTrue_ReturnsTrue()
{
var service = new ApplicationStateService();
service.IsMonitoringActive = true;
Assert.True(service.IsMonitoringActive);
}
// === Browser State ===
[Fact]
public void BrowserCategoryIndex_DefaultZero()
{
var service = new ApplicationStateService();
Assert.Equal(0, service.BrowserCategoryIndex);
}
[Fact]
public void BrowserSearchQuery_DefaultEmpty()
{
var service = new ApplicationStateService();
Assert.Equal("", service.BrowserSearchQuery);
}
[Fact]
public void BrowserCategoryIndex_SetAndGet()
{
var service = new ApplicationStateService();
service.BrowserCategoryIndex = 5;
Assert.Equal(5, service.BrowserCategoryIndex);
}
[Fact]
public void BrowserSearchQuery_SetAndGet()
{
var service = new ApplicationStateService();
service.BrowserSearchQuery = "iphone";
Assert.Equal("iphone", service.BrowserSearchQuery);
}
// === Direct References ===
[Fact]
public void GetAuctionsDirectRef_ReturnsSameList()
{
var service = new ApplicationStateService();
service.AddAuction(new AuctionInfo { AuctionId = "1" });
var direct = service.GetAuctionsDirectRef();
Assert.Single(direct);
}
[Fact]
public void GetLogDirectRef_ReturnsSameList()
{
var service = new ApplicationStateService();
service.AddLog("test");
var direct = service.GetLogDirectRef();
Assert.Single(direct);
}
// === SessionStart ===
[Fact]
public void SessionStart_ReturnsNonDefault()
{
var service = new ApplicationStateService();
Assert.True(service.SessionStart > DateTime.MinValue);
}
}
@@ -0,0 +1,148 @@
using AutoBidder.Models;
using AutoBidder.Services;
using Xunit;
namespace AutoBidder.Tests;
public class AuctionMonitorProductValueExtensionsTests
{
// === IsStillWorthBidding ===
[Fact]
public void IsStillWorthBidding_NoCalculatedValue_ReturnsTrue()
{
var auction = new AuctionInfo { AuctionId = "1", CalculatedValue = null };
Assert.True(AuctionMonitorProductValueExtensions.IsStillWorthBidding(auction));
}
[Fact]
public void IsStillWorthBidding_NoBuyNowPrice_ReturnsTrue()
{
var auction = new AuctionInfo
{
AuctionId = "1",
CalculatedValue = new ProductValue { BuyNowPrice = null }
};
Assert.True(AuctionMonitorProductValueExtensions.IsStillWorthBidding(auction));
}
[Fact]
public void IsStillWorthBidding_NoSavings_ReturnsTrue()
{
var auction = new AuctionInfo
{
AuctionId = "1",
CalculatedValue = new ProductValue { BuyNowPrice = 100.0, Savings = null }
};
Assert.True(AuctionMonitorProductValueExtensions.IsStillWorthBidding(auction));
}
[Fact]
public void IsStillWorthBidding_PositiveSavings_ReturnsTrue()
{
var auction = new AuctionInfo
{
AuctionId = "1",
CalculatedValue = new ProductValue
{
BuyNowPrice = 100.0,
Savings = 50.0,
SavingsPercentage = 50.0
}
};
Assert.True(AuctionMonitorProductValueExtensions.IsStillWorthBidding(auction, minSavingsPercentage: 10));
}
[Fact]
public void IsStillWorthBidding_NegativeSavingsBelowThreshold_ReturnsFalse()
{
var auction = new AuctionInfo
{
AuctionId = "1",
CalculatedValue = new ProductValue
{
BuyNowPrice = 100.0,
Savings = -20.0,
SavingsPercentage = -20.0
}
};
Assert.False(AuctionMonitorProductValueExtensions.IsStillWorthBidding(auction, minSavingsPercentage: 0));
}
[Fact]
public void IsStillWorthBidding_SavingsExactlyAtThreshold_ReturnsTrue()
{
var auction = new AuctionInfo
{
AuctionId = "1",
CalculatedValue = new ProductValue
{
BuyNowPrice = 100.0,
Savings = 10.0,
SavingsPercentage = 10.0
}
};
Assert.True(AuctionMonitorProductValueExtensions.IsStillWorthBidding(auction, minSavingsPercentage: 10.0));
}
// === UpdateProductValue ===
[Fact]
public void UpdateProductValue_SetsCalculatedValue()
{
var auction = new AuctionInfo
{
AuctionId = "1",
BidCost = 0.20,
BuyNowPrice = 50.0,
ShippingCost = 5.0,
BidsUsedOnThisAuction = 10
};
var state = new AuctionState { Price = 2.0 };
AuctionMonitorProductValueExtensions.UpdateProductValue(auction, state);
Assert.NotNull(auction.CalculatedValue);
Assert.Equal(2.0, auction.CalculatedValue!.CurrentPrice);
}
[Fact]
public void UpdateProductValue_TotalBidsBasedOnPrice()
{
var auction = new AuctionInfo
{
AuctionId = "1",
BidCost = 0.20,
BidsUsedOnThisAuction = 0
};
var state = new AuctionState { Price = 1.50 }; // 150 total bids
AuctionMonitorProductValueExtensions.UpdateProductValue(auction, state);
Assert.Equal(150, auction.CalculatedValue!.TotalBids);
}
[Fact]
public void UpdateProductValue_WithLogValue_DoesNotThrow()
{
var auction = new AuctionInfo
{
AuctionId = "1",
BidCost = 0.20,
BuyNowPrice = 100.0,
BidsUsedOnThisAuction = 5
};
var state = new AuctionState { Price = 1.0 };
// Should not throw
AuctionMonitorProductValueExtensions.UpdateProductValue(auction, state, logValue: true);
Assert.NotNull(auction.CalculatedValue);
}
}
@@ -0,0 +1,155 @@
using AutoBidder.Models;
using AutoBidder.Services;
using Xunit;
namespace AutoBidder.Tests;
public class AuctionStateServiceTests
{
// === GetAuction ===
[Fact]
public void GetAuction_NonExistentId_ReturnsNull()
{
var service = new AuctionStateService();
Assert.Null(service.GetAuction("non-existent"));
}
// === AddAuction ===
[Fact]
public void AddAuction_NewAuction_CanBeRetrieved()
{
var service = new AuctionStateService();
var auction = new AuctionInfo { AuctionId = "123", Name = "Test" };
service.AddAuction(auction);
var retrieved = service.GetAuction("123");
Assert.NotNull(retrieved);
Assert.Equal("Test", retrieved!.Name);
}
[Fact]
public void AddAuction_DuplicateId_DoesNotAdd()
{
var service = new AuctionStateService();
var auction1 = new AuctionInfo { AuctionId = "123", Name = "First" };
var auction2 = new AuctionInfo { AuctionId = "123", Name = "Second" };
service.AddAuction(auction1);
service.AddAuction(auction2);
Assert.Equal("First", service.GetAuction("123")!.Name);
}
[Fact]
public void AddAuction_FiresOnAuctionAddedEvent()
{
var service = new AuctionStateService();
string? addedId = null;
service.OnAuctionAdded += id => addedId = id;
service.AddAuction(new AuctionInfo { AuctionId = "123" });
Assert.Equal("123", addedId);
}
[Fact]
public void AddAuction_FiresOnStateChangedEvent()
{
var service = new AuctionStateService();
var fired = false;
service.OnStateChanged += () => fired = true;
service.AddAuction(new AuctionInfo { AuctionId = "123" });
Assert.True(fired);
}
// === RemoveAuction ===
[Fact]
public void RemoveAuction_ExistingAuction_Removes()
{
var service = new AuctionStateService();
service.AddAuction(new AuctionInfo { AuctionId = "123" });
service.RemoveAuction("123");
Assert.Null(service.GetAuction("123"));
}
[Fact]
public void RemoveAuction_NonExistent_DoesNotThrow()
{
var service = new AuctionStateService();
service.RemoveAuction("non-existent"); // Should not throw
}
[Fact]
public void RemoveAuction_FiresOnAuctionRemovedEvent()
{
var service = new AuctionStateService();
service.AddAuction(new AuctionInfo { AuctionId = "123" });
string? removedId = null;
service.OnAuctionRemoved += id => removedId = id;
service.RemoveAuction("123");
Assert.Equal("123", removedId);
}
// === UpdateAuction ===
[Fact]
public void UpdateAuction_ExistingAuction_AppliesUpdateAction()
{
var service = new AuctionStateService();
service.AddAuction(new AuctionInfo { AuctionId = "123", Name = "Before" });
service.UpdateAuction("123", a => a.Name = "After");
Assert.Equal("After", service.GetAuction("123")!.Name);
}
[Fact]
public void UpdateAuction_NonExistent_DoesNotThrow()
{
var service = new AuctionStateService();
service.UpdateAuction("non-existent", a => a.Name = "test"); // Should not throw
}
[Fact]
public void UpdateAuction_FiresOnAuctionUpdatedEvent()
{
var service = new AuctionStateService();
service.AddAuction(new AuctionInfo { AuctionId = "123" });
string? updatedId = null;
service.OnAuctionUpdated += id => updatedId = id;
service.UpdateAuction("123", a => a.Name = "Updated");
Assert.Equal("123", updatedId);
}
// === GetAllAuctions ===
[Fact]
public void GetAllAuctions_Empty_ReturnsEmpty()
{
var service = new AuctionStateService();
Assert.Empty(service.GetAllAuctions());
}
[Fact]
public void GetAllAuctions_MultipleAuctions_ReturnsAll()
{
var service = new AuctionStateService();
service.AddAuction(new AuctionInfo { AuctionId = "1" });
service.AddAuction(new AuctionInfo { AuctionId = "2" });
service.AddAuction(new AuctionInfo { AuctionId = "3" });
Assert.Equal(3, service.GetAllAuctions().Count());
}
}
@@ -0,0 +1,192 @@
using AutoBidder.Models;
using Xunit;
namespace AutoBidder.Tests;
public class AuctionStatisticsTests
{
// === Calculate ===
[Fact]
public void Calculate_EmptyBidHistory_ReturnsZeroStats()
{
var auction = new AuctionInfo
{
AuctionId = "1",
Name = "Test",
AddedAt = DateTime.UtcNow.AddMinutes(-10)
};
var stats = AuctionStatistics.Calculate(auction);
Assert.Equal("1", stats.AuctionId);
Assert.Equal("Test", stats.Name);
Assert.Equal(0, stats.TotalBids);
Assert.Equal(0, stats.MyBids);
}
[Fact]
public void Calculate_WithBidHistory_CountsMyBids()
{
var auction = new AuctionInfo
{
AuctionId = "1",
Name = "Test",
AddedAt = DateTime.UtcNow.AddMinutes(-10),
BidHistory = new List<BidHistory>
{
new() { EventType = BidEventType.MyBid, Price = 1.0, Timer = 5 },
new() { EventType = BidEventType.MyBid, Price = 2.0, Timer = 4 },
new() { EventType = BidEventType.OpponentBid, Price = 3.0, Timer = 3 },
}
};
var stats = AuctionStatistics.Calculate(auction);
Assert.Equal(2, stats.MyBids);
Assert.Equal(3, stats.TotalBids);
Assert.Equal(1, stats.OpponentBids);
}
[Fact]
public void Calculate_WithPrices_ComputesPriceStats()
{
var auction = new AuctionInfo
{
AuctionId = "1",
Name = "Test",
AddedAt = DateTime.UtcNow.AddMinutes(-10),
BidHistory = new List<BidHistory>
{
new() { EventType = BidEventType.MyBid, Price = 1.0 },
new() { EventType = BidEventType.OpponentBid, Price = 3.0 },
new() { EventType = BidEventType.MyBid, Price = 5.0 },
}
};
var stats = AuctionStatistics.Calculate(auction);
Assert.Equal(1.0, stats.StartPrice);
Assert.Equal(5.0, stats.CurrentPrice);
Assert.Equal(1.0, stats.MinPrice);
Assert.Equal(5.0, stats.MaxPrice);
Assert.Equal(3.0, stats.AvgPrice, 2);
}
[Fact]
public void Calculate_WithLatency_ComputesLatencyStats()
{
var auction = new AuctionInfo
{
AuctionId = "1",
Name = "Test",
AddedAt = DateTime.UtcNow.AddMinutes(-10),
BidHistory = new List<BidHistory>
{
new() { EventType = BidEventType.MyBid, Price = 1.0, LatencyMs = 50 },
new() { EventType = BidEventType.MyBid, Price = 2.0, LatencyMs = 100 },
}
};
var stats = AuctionStatistics.Calculate(auction);
Assert.Equal(75, stats.AvgClickLatencyMs);
Assert.Equal(50, stats.MinClickLatencyMs);
Assert.Equal(100, stats.MaxClickLatencyMs);
}
[Fact]
public void Calculate_WithBidderStats_FindsMostActive()
{
var auction = new AuctionInfo
{
AuctionId = "1",
Name = "Test",
AddedAt = DateTime.UtcNow.AddMinutes(-10),
BidderStats = new Dictionary<string, BidderInfo>(StringComparer.OrdinalIgnoreCase)
{
["alice"] = new BidderInfo { Username = "alice", BidCount = 10 },
["bob"] = new BidderInfo { Username = "bob", BidCount = 25 },
}
};
var stats = AuctionStatistics.Calculate(auction);
Assert.Equal("bob", stats.MostActiveBidder);
Assert.Equal(25, stats.MostActiveBidderCount);
Assert.Equal(2, stats.UniqueBidders);
}
[Fact]
public void Calculate_SetsResets()
{
var auction = new AuctionInfo
{
AuctionId = "1",
Name = "Test",
ResetCount = 42,
AddedAt = DateTime.UtcNow.AddMinutes(-60)
};
var stats = AuctionStatistics.Calculate(auction);
Assert.Equal(42, stats.Resets);
}
[Fact]
public void Calculate_MonitoringDuration_IsPositive()
{
var auction = new AuctionInfo
{
AuctionId = "1",
Name = "Test",
AddedAt = DateTime.UtcNow.AddHours(-2)
};
var stats = AuctionStatistics.Calculate(auction);
Assert.True(stats.MonitoringDuration.TotalHours >= 1.9);
}
[Fact]
public void Calculate_WithBidsAndDuration_ComputesBidsPerMinute()
{
var auction = new AuctionInfo
{
AuctionId = "1",
Name = "Test",
AddedAt = DateTime.UtcNow.AddMinutes(-10),
BidHistory = new List<BidHistory>
{
new() { EventType = BidEventType.MyBid, Price = 1.0 },
new() { EventType = BidEventType.OpponentBid, Price = 2.0 },
}
};
var stats = AuctionStatistics.Calculate(auction);
Assert.True(stats.BidsPerMinute > 0);
}
[Fact]
public void Calculate_MyBidSuccessRate_CorrectPercentage()
{
var auction = new AuctionInfo
{
AuctionId = "1",
Name = "Test",
AddedAt = DateTime.UtcNow.AddMinutes(-10),
BidHistory = new List<BidHistory>
{
new() { EventType = BidEventType.MyBid, Price = 1.0 },
new() { EventType = BidEventType.OpponentBid, Price = 2.0 },
new() { EventType = BidEventType.OpponentBid, Price = 3.0 },
new() { EventType = BidEventType.OpponentBid, Price = 4.0 },
}
};
var stats = AuctionStatistics.Calculate(auction);
Assert.Equal(25.0, stats.MyBidSuccessRate, 1); // 1 out of 4
}
}
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\AutoBidder.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,303 @@
using AutoBidder.Models;
using AutoBidder.Services;
using AutoBidder.Utilities;
using Xunit;
namespace AutoBidder.Tests;
public class BidStrategyServiceTests
{
private readonly BidStrategyService _service = new();
// === UpdateHeatMetric ===
[Fact]
public void UpdateHeatMetric_CompetitionDisabled_DoesNothing()
{
var auction = CreateAuction();
var settings = new AppSettings { CompetitionDetectionEnabled = false };
_service.UpdateHeatMetric(auction, settings, "me");
Assert.Equal(0, auction.HeatMetric);
}
[Fact]
public void UpdateHeatMetric_WithRecentBids_CalculatesHeat()
{
var auction = CreateAuction();
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
auction.RecentBids.Add(new BidHistoryEntry { Username = "alice", Timestamp = now - 5, Price = 1 });
auction.RecentBids.Add(new BidHistoryEntry { Username = "bob", Timestamp = now - 5, Price = 2 });
auction.RecentBids.Add(new BidHistoryEntry { Username = "charlie", Timestamp = now - 10, Price = 3 });
var settings = new AppSettings
{
CompetitionDetectionEnabled = true,
CompetitionWindowSeconds = 30,
OpponentProfilingEnabled = false
};
_service.UpdateHeatMetric(auction, settings, "me");
Assert.True(auction.ActiveBiddersCount >= 3);
Assert.True(auction.HeatMetric > 0);
}
[Fact]
public void UpdateHeatMetric_ExcludesCurrentUser()
{
var auction = CreateAuction();
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
auction.RecentBids.Add(new BidHistoryEntry { Username = "me", Timestamp = now - 5, Price = 1 });
auction.RecentBids.Add(new BidHistoryEntry { Username = "me", Timestamp = now - 10, Price = 2 });
var settings = new AppSettings
{
CompetitionDetectionEnabled = true,
CompetitionWindowSeconds = 30,
OpponentProfilingEnabled = false
};
_service.UpdateHeatMetric(auction, settings, "me");
Assert.Equal(0, auction.ActiveBiddersCount);
}
[Fact]
public void UpdateHeatMetric_DetectsCollisions()
{
var auction = CreateAuction();
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
// Two bids at the same second = collision
auction.RecentBids.Add(new BidHistoryEntry { Username = "alice", Timestamp = now - 5, Price = 1 });
auction.RecentBids.Add(new BidHistoryEntry { Username = "bob", Timestamp = now - 5, Price = 2 });
var settings = new AppSettings
{
CompetitionDetectionEnabled = true,
CompetitionWindowSeconds = 30,
OpponentProfilingEnabled = false
};
_service.UpdateHeatMetric(auction, settings, "me");
Assert.True(auction.CollisionCount >= 1);
}
// === ShouldPlaceBid ===
[Fact]
public void ShouldPlaceBid_AdvancedStrategiesDisabled_ReturnsTrue()
{
var auction = CreateAuction();
auction.AdvancedStrategiesEnabled = false;
var state = new AuctionState { Price = 1.0, LastBidder = "other" };
var settings = new AppSettings();
var decision = _service.ShouldPlaceBid(auction, state, settings, "me");
Assert.True(decision.ShouldBid);
}
[Fact]
public void ShouldPlaceBid_InSoftRetreat_ReturnsFalse()
{
var auction = CreateAuction();
auction.IsInSoftRetreat = true;
auction.LastSoftRetreatAt = DateTime.UtcNow;
var state = new AuctionState { Price = 1.0 };
var settings = new AppSettings
{
SoftRetreatEnabled = true,
SoftRetreatDurationSeconds = 300 // Long retreat
};
var decision = _service.ShouldPlaceBid(auction, state, settings, "me");
Assert.False(decision.ShouldBid);
Assert.Contains("Soft retreat", decision.Reason);
}
[Fact]
public void ShouldPlaceBid_SoftRetreatExpired_ReturnsTrue()
{
var auction = CreateAuction();
auction.IsInSoftRetreat = true;
auction.LastSoftRetreatAt = DateTime.UtcNow.AddSeconds(-100);
var state = new AuctionState { Price = 1.0 };
var settings = new AppSettings
{
SoftRetreatEnabled = true,
SoftRetreatDurationSeconds = 10,
CompetitionDetectionEnabled = false,
OpponentProfilingEnabled = false,
ProbabilisticBiddingEnabled = false,
BankrollManagerEnabled = false,
AntiBotDetectionEnabled = false,
UserExhaustionEnabled = false
};
var decision = _service.ShouldPlaceBid(auction, state, settings, "me");
Assert.True(decision.ShouldBid);
Assert.False(auction.IsInSoftRetreat);
}
[Fact]
public void ShouldPlaceBid_TooManyCollisions_ActivatesSoftRetreat()
{
var auction = CreateAuction();
auction.ConsecutiveCollisions = 5;
var state = new AuctionState { Price = 1.0 };
var settings = new AppSettings
{
SoftRetreatEnabled = true,
SoftRetreatAfterCollisions = 3,
SoftRetreatDurationSeconds = 30,
CompetitionDetectionEnabled = false,
OpponentProfilingEnabled = false,
ProbabilisticBiddingEnabled = false,
BankrollManagerEnabled = false,
AntiBotDetectionEnabled = false,
UserExhaustionEnabled = false
};
var decision = _service.ShouldPlaceBid(auction, state, settings, "me");
Assert.False(decision.ShouldBid);
Assert.True(auction.IsInSoftRetreat);
}
[Fact]
public void ShouldPlaceBid_AggressiveBiddersAvoid_ReturnsFalse()
{
var auction = CreateAuction();
auction.AggressiveBidders.Add("aggro_user");
var state = new AuctionState { Price = 1.0 };
var settings = new AppSettings
{
SoftRetreatEnabled = false,
CompetitionDetectionEnabled = false,
OpponentProfilingEnabled = true,
AggressiveBidderAction = "Avoid",
ProbabilisticBiddingEnabled = false,
BankrollManagerEnabled = false,
AntiBotDetectionEnabled = false,
UserExhaustionEnabled = false
};
var decision = _service.ShouldPlaceBid(auction, state, settings, "me");
Assert.False(decision.ShouldBid);
Assert.Contains("aggressivi", decision.Reason);
}
[Fact]
public void ShouldPlaceBid_BankrollLimitReached_ReturnsFalse()
{
var auction = CreateAuction();
auction.SessionBidCount = 100;
auction.MaxBidsOverride = 50;
var state = new AuctionState { Price = 1.0 };
var settings = new AppSettings
{
SoftRetreatEnabled = false,
CompetitionDetectionEnabled = false,
OpponentProfilingEnabled = false,
ProbabilisticBiddingEnabled = false,
BankrollManagerEnabled = true,
MaxBidsPerAuction = 50,
AntiBotDetectionEnabled = false,
UserExhaustionEnabled = false
};
var decision = _service.ShouldPlaceBid(auction, state, settings, "me");
Assert.False(decision.ShouldBid);
}
// === RecordBidAttempt ===
[Fact]
public void RecordBidAttempt_Success_IncrementsCountsAndResetsCollisions()
{
var auction = CreateAuction();
auction.ConsecutiveCollisions = 3;
_service.RecordBidAttempt(auction, success: true, collision: false);
Assert.Equal(1, auction.SessionBidCount);
Assert.Equal(1, auction.SuccessfulBidCount);
Assert.Equal(0, auction.ConsecutiveCollisions);
}
[Fact]
public void RecordBidAttempt_FailureWithCollision_IncrementsCollisionCounters()
{
var auction = CreateAuction();
_service.RecordBidAttempt(auction, success: false, collision: true);
Assert.Equal(1, auction.SessionBidCount);
Assert.Equal(1, auction.FailedBidCount);
Assert.Equal(1, auction.CollisionCount);
Assert.Equal(1, auction.ConsecutiveCollisions);
}
// === RecordTimerExpired ===
[Fact]
public void RecordTimerExpired_IncrementsCounters()
{
var auction = CreateAuction();
_service.RecordTimerExpired(auction);
Assert.Equal(1, auction.TimerExpiredCount);
Assert.Equal(1, auction.ConsecutiveCollisions);
}
// === ResetSession ===
[Fact]
public void ResetSession_ClearsSessionTotals()
{
var auction = CreateAuction();
_service.RecordBidAttempt(auction, true);
_service.RecordBidAttempt(auction, true);
_service.ResetSession();
var stats = _service.GetSessionStats();
Assert.Equal(0, stats.TotalBids);
}
// === GetSessionStats ===
[Fact]
public void GetSessionStats_ReturnsCorrectTotals()
{
var auction = CreateAuction();
_service.RecordBidAttempt(auction, true);
_service.RecordBidAttempt(auction, false);
var stats = _service.GetSessionStats();
Assert.Equal(2, stats.TotalBids);
Assert.True(stats.SessionDuration >= TimeSpan.Zero);
}
// === Helpers ===
private static AuctionInfo CreateAuction()
{
return new AuctionInfo
{
AuctionId = "test",
Name = "Test Auction",
OriginalUrl = "https://test.com",
AdvancedStrategiesEnabled = true
};
}
}
@@ -0,0 +1,187 @@
using AutoBidder.Services;
using Xunit;
namespace AutoBidder.Tests;
public class ClosedAuctionsScraperTests
{
// The scraper's parsing methods are private, but we can test them
// indirectly through the public ScrapeYieldAsync and ScrapeAsync methods
// using a mock HTTP handler.
// === Constructor ===
[Fact]
public void Constructor_DefaultHandler_DoesNotThrow()
{
var scraper = new ClosedAuctionsScraper();
Assert.NotNull(scraper);
}
[Fact]
public void Constructor_WithLogCallback_DoesNotThrow()
{
var messages = new List<string>();
var scraper = new ClosedAuctionsScraper(log: msg => messages.Add(msg));
Assert.NotNull(scraper);
}
// === ScrapeAsync with mock handler ===
[Fact]
public async Task ScrapeAsync_EmptyUrl_ThrowsArgumentNullException()
{
var scraper = new ClosedAuctionsScraper();
await Assert.ThrowsAsync<ArgumentNullException>(() => scraper.ScrapeAsync(""));
}
[Fact]
public async Task ScrapeAsync_WhitespaceUrl_ThrowsArgumentNullException()
{
var scraper = new ClosedAuctionsScraper();
await Assert.ThrowsAsync<ArgumentNullException>(() => scraper.ScrapeAsync(" "));
}
[Fact]
public async Task ScrapeAsync_WithMockHandler_ParsesAuctionLinks()
{
var closedPageHtml = @"
<html>
<body>
<div data-href=""/asta/iphone-15-12345"">
<b class=""media-heading""><a href=""#"">iPhone 15 Pro</a></b>
<span class=""price"">5,50 €</span>
<span class=""mobile_offerer offer"">winner123</span>
</div>
</body>
</html>";
var auctionPageHtml = @"
<html>
<head><title>iPhone 15 Pro - Asta Bidoo</title></head>
<body>
<h1>iPhone 15 Pro</h1>
<span class=""price"">5,50 €</span>
<span>Vincitore: <a>winner123</a></span>
<p class=""bids-used""><span>128</span> Puntate utilizzate</p>
</body>
</html>";
var handler = new MockHttpHandler(new Dictionary<string, string>
{
["https://it.bidoo.com/closed-auctions"] = closedPageHtml,
["https://it.bidoo.com/asta/iphone-15-12345"] = auctionPageHtml
});
var scraper = new ClosedAuctionsScraper(handler);
var results = await scraper.ScrapeAsync("https://it.bidoo.com/closed-auctions");
Assert.NotEmpty(results);
var first = results[0];
Assert.NotNull(first.ProductName);
Assert.NotNull(first.AuctionUrl);
}
[Fact]
public async Task ScrapeYieldAsync_ProducesRecordsIncrementally()
{
var closedPageHtml = @"
<html>
<body>
<div data-href=""/asta/test-1""></div>
<div data-href=""/asta/test-2""></div>
</body>
</html>";
var auctionHtml = @"<html><head><title>Test</title></head><body><h1>Test Product</h1><span class=""price"">1,00 €</span></body></html>";
var handler = new MockHttpHandler(new Dictionary<string, string>
{
["https://it.bidoo.com/closed"] = closedPageHtml,
["https://it.bidoo.com/asta/test-1"] = auctionHtml,
["https://it.bidoo.com/asta/test-2"] = auctionHtml
});
var scraper = new ClosedAuctionsScraper(handler);
var count = 0;
await foreach (var record in scraper.ScrapeYieldAsync("https://it.bidoo.com/closed"))
{
count++;
Assert.NotNull(record);
}
Assert.Equal(2, count);
}
[Fact]
public async Task ScrapeAsync_FailedAuctionPage_ReturnsParseErrorRecord()
{
var closedPageHtml = @"<html><body><div data-href=""/asta/fail-1""></div></body></html>";
var handler = new MockHttpHandler(new Dictionary<string, string>
{
["https://it.bidoo.com/closed"] = closedPageHtml
// Intentionally no mapping for /asta/fail-1 -> will fail
});
var scraper = new ClosedAuctionsScraper(handler);
var results = await scraper.ScrapeAsync("https://it.bidoo.com/closed");
Assert.Single(results);
Assert.Equal("(parse error)", results[0].ProductName);
}
[Fact]
public async Task ScrapeAsync_ExtractsBidsUsed()
{
var closedPageHtml = @"<html><body><div data-href=""/asta/bid-test""></div></body></html>";
var auctionHtml = @"
<html><body>
<h1>Test Product</h1>
<p class=""bids-used""> puntate<span>456</span> Puntate utilizzate</p>
</body></html>";
var handler = new MockHttpHandler(new Dictionary<string, string>
{
["https://it.bidoo.com/closed"] = closedPageHtml,
["https://it.bidoo.com/asta/bid-test"] = auctionHtml
});
var scraper = new ClosedAuctionsScraper(handler);
var results = await scraper.ScrapeAsync("https://it.bidoo.com/closed");
Assert.Single(results);
Assert.Equal(456, results[0].BidsUsed);
}
/// <summary>
/// Simple mock HTTP handler for testing
/// </summary>
private class MockHttpHandler : HttpMessageHandler
{
private readonly Dictionary<string, string> _responses;
public MockHttpHandler(Dictionary<string, string> responses)
{
_responses = responses;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var url = request.RequestUri!.ToString();
if (_responses.TryGetValue(url, out var content))
{
return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(content, System.Text.Encoding.UTF8, "text/html")
});
}
return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.NotFound)
{
Content = new StringContent("Not Found")
});
}
}
}
@@ -0,0 +1,77 @@
using AutoBidder.Services;
using Xunit;
namespace AutoBidder.Tests;
public class HtmlCacheServiceTests
{
// === Constructor / GetStats ===
[Fact]
public void Constructor_InitializesWithDefaults()
{
var service = new HtmlCacheService();
var stats = service.GetStats();
Assert.Equal(0, stats.TotalEntries);
Assert.True(stats.AvailableSlots > 0);
Assert.True(stats.MaxConcurrent > 0);
}
[Fact]
public void Constructor_CustomParameters_ReflectsInStats()
{
var service = new HtmlCacheService(maxConcurrentRequests: 5);
var stats = service.GetStats();
Assert.Equal(5, stats.MaxConcurrent);
Assert.Equal(5, stats.AvailableSlots);
}
// === ClearCache ===
[Fact]
public void ClearCache_EmptyCache_DoesNotThrow()
{
var service = new HtmlCacheService();
service.ClearCache();
Assert.Equal(0, service.GetStats().TotalEntries);
}
// === CleanExpiredCache ===
[Fact]
public void CleanExpiredCache_EmptyCache_DoesNotThrow()
{
var service = new HtmlCacheService();
service.CleanExpiredCache();
Assert.Equal(0, service.GetStats().TotalEntries);
}
// === OnLog callback ===
[Fact]
public void ClearCache_InvokesOnLog()
{
var service = new HtmlCacheService();
var logged = false;
service.OnLog = _ => logged = true;
service.ClearCache();
Assert.True(logged);
}
// === GetStats ===
[Fact]
public void GetStats_ReturnsNonNullResult()
{
var service = new HtmlCacheService();
var stats = service.GetStats();
Assert.NotNull(stats);
}
}
@@ -0,0 +1,314 @@
using AutoBidder.Models;
using Xunit;
namespace AutoBidder.Tests;
public class ModelTests
{
// === BidooSession.IsValid ===
[Fact]
public void BidooSession_IsValid_FalseWhenEmpty()
{
var session = new BidooSession();
Assert.False(session.IsValid);
}
[Fact]
public void BidooSession_IsValid_TrueWithAuthToken()
{
var session = new BidooSession { AuthToken = "abc123" };
Assert.True(session.IsValid);
}
[Fact]
public void BidooSession_IsValid_TrueWithCookieString()
{
var session = new BidooSession { CookieString = "session=abc" };
Assert.True(session.IsValid);
}
[Fact]
public void BidooSession_IsValid_FalseWithWhitespaceOnly()
{
var session = new BidooSession { AuthToken = " ", CookieString = " " };
Assert.False(session.IsValid);
}
// === ProductStatisticsRecord.WinRate ===
[Fact]
public void ProductStatisticsRecord_WinRate_ZeroWhenNoAuctions()
{
var record = new ProductStatisticsRecord { TotalAuctions = 0, WonAuctions = 0 };
Assert.Equal(0, record.WinRate);
}
[Fact]
public void ProductStatisticsRecord_WinRate_CorrectPercentage()
{
var record = new ProductStatisticsRecord { TotalAuctions = 10, WonAuctions = 3 };
Assert.Equal(30.0, record.WinRate, 1);
}
[Fact]
public void ProductStatisticsRecord_WinRate_100Percent()
{
var record = new ProductStatisticsRecord { TotalAuctions = 5, WonAuctions = 5 };
Assert.Equal(100.0, record.WinRate, 1);
}
// === HourlyStats.WinRate ===
[Fact]
public void HourlyStats_WinRate_ZeroWhenNoAuctions()
{
var stats = new HourlyStats { TotalAuctions = 0, WonAuctions = 0 };
Assert.Equal(0, stats.WinRate);
}
[Fact]
public void HourlyStats_WinRate_CorrectPercentage()
{
var stats = new HourlyStats { TotalAuctions = 20, WonAuctions = 5 };
Assert.Equal(25.0, stats.WinRate, 1);
}
// === CompleteAuctionHistoryRecord ===
[Fact]
public void CompleteAuctionHistoryRecord_DurationFormatted_WithDuration()
{
var record = new CompleteAuctionHistoryRecord { DurationSeconds = 3661 }; // 1h 1m 1s
Assert.Equal("01:01:01", record.DurationFormatted);
}
[Fact]
public void CompleteAuctionHistoryRecord_DurationFormatted_NoDuration()
{
var record = new CompleteAuctionHistoryRecord { DurationSeconds = null };
Assert.Equal("-", record.DurationFormatted);
}
[Fact]
public void CompleteAuctionHistoryRecord_SuccessRate_ZeroWhenNoBids()
{
var record = new CompleteAuctionHistoryRecord { MySuccessfulBids = 0, MyFailedBids = 0 };
Assert.Equal(0, record.SuccessRate);
}
[Fact]
public void CompleteAuctionHistoryRecord_SuccessRate_CorrectPercentage()
{
var record = new CompleteAuctionHistoryRecord { MySuccessfulBids = 7, MyFailedBids = 3 };
Assert.Equal(70.0, record.SuccessRate, 1);
}
// === ProductStat ===
[Fact]
public void ProductStat_AverageBidsUsed_ZeroWhenNoAuctions()
{
var stat = new ProductStat { TotalAuctions = 0, TotalBidsUsed = 0 };
Assert.Equal(0, stat.AverageBidsUsed);
}
[Fact]
public void ProductStat_AverageBidsUsed_CorrectAverage()
{
var stat = new ProductStat { TotalAuctions = 5, TotalBidsUsed = 100 };
Assert.Equal(20.0, stat.AverageBidsUsed, 1);
}
[Fact]
public void ProductStat_AverageFinalPrice_ZeroWhenNoAuctions()
{
var stat = new ProductStat { TotalAuctions = 0, TotalFinalPriceCents = 0 };
Assert.Equal(0, stat.AverageFinalPrice);
}
[Fact]
public void ProductStat_AverageFinalPrice_CorrectAverage()
{
var stat = new ProductStat { TotalAuctions = 2, TotalFinalPriceCents = 1000 };
Assert.Equal(5.0, stat.AverageFinalPrice, 2); // 1000 cents / 100 / 2 = 5.0
}
// === BidHistoryEntry ===
[Fact]
public void BidHistoryEntry_PriceFormatted_TwoDecimals()
{
var entry = new BidHistoryEntry { Price = 5.5m };
// PriceFormatted uses default culture formatting
Assert.Equal(5.5m.ToString("0.00"), entry.PriceFormatted);
}
[Fact]
public void BidHistoryEntry_TimeFormatted_ValidFormat()
{
var entry = new BidHistoryEntry { Timestamp = 1700000000 };
// Should produce a valid HH:mm:ss string
Assert.Matches(@"^\d{2}:\d{2}:\d{2}$", entry.TimeFormatted);
}
// === BidderInfo ===
[Fact]
public void BidderInfo_LastBidTimeDisplay_DashWhenDefault()
{
var info = new BidderInfo();
Assert.Equal("-", info.LastBidTimeDisplay);
}
[Fact]
public void BidderInfo_LastBidTimeDisplay_FormattedWhenSet()
{
var info = new BidderInfo { LastBidTime = new DateTime(2024, 1, 1, 14, 30, 45) };
Assert.Equal("14:30:45", info.LastBidTimeDisplay);
}
// === AuctionLogEntry ===
[Fact]
public void AuctionLogEntry_LevelIcon_ReturnsBootstrapClass()
{
var entry = new AuctionLogEntry { Level = AuctionLogLevel.Error };
Assert.Contains("bi-", entry.LevelIcon);
}
[Fact]
public void AuctionLogEntry_LevelClass_ReturnsCorrectCssClass()
{
var entry = new AuctionLogEntry { Level = AuctionLogLevel.Error };
Assert.Equal("alog-error", entry.LevelClass);
}
[Fact]
public void AuctionLogEntry_LevelLabel_ReturnsShortLabel()
{
Assert.Equal("ERR", new AuctionLogEntry { Level = AuctionLogLevel.Error }.LevelLabel);
Assert.Equal("WARN", new AuctionLogEntry { Level = AuctionLogLevel.Warning }.LevelLabel);
Assert.Equal("OK", new AuctionLogEntry { Level = AuctionLogLevel.Success }.LevelLabel);
Assert.Equal("BID", new AuctionLogEntry { Level = AuctionLogLevel.Bid }.LevelLabel);
}
[Fact]
public void AuctionLogEntry_CategoryLabel_ReturnsLabel()
{
var entry = new AuctionLogEntry { Category = AuctionLogCategory.BidAttempt };
Assert.Equal("Puntata", entry.CategoryLabel);
}
[Fact]
public void AuctionLogEntry_TimeDisplay_FormattedWithMs()
{
var entry = new AuctionLogEntry { Timestamp = new DateTime(2024, 1, 1, 14, 30, 45, 123) };
Assert.Equal("14:30:45.123", entry.TimeDisplay);
}
// === AuctionInfo.AddLog ===
[Fact]
public void AuctionInfo_AddLog_AddsEntry()
{
var auction = new AuctionInfo { AuctionId = "1" };
auction.AddLog("[BID] Test bid placed");
Assert.Single(auction.AuctionLog);
}
[Fact]
public void AuctionInfo_AddLog_ParsesTag()
{
var auction = new AuctionInfo { AuctionId = "1" };
auction.AddLog("[ERROR] Something went wrong");
Assert.Equal(AuctionLogLevel.Error, auction.AuctionLog[0].Level);
}
[Fact]
public void AuctionInfo_AddLog_Deduplicates()
{
var auction = new AuctionInfo { AuctionId = "1" };
auction.AddLog("[BID] Same message");
auction.AddLog("[BID] Same message");
auction.AddLog("[BID] Same message");
Assert.Single(auction.AuctionLog);
Assert.Equal(3, auction.AuctionLog[0].RepeatCount);
}
[Fact]
public void AuctionInfo_AddLog_TruncatesAtMaxLines()
{
var auction = new AuctionInfo { AuctionId = "1" };
for (int i = 0; i < 300; i++)
{
auction.AddLog($"Message {i}");
}
Assert.True(auction.AuctionLog.Count <= 200);
}
[Fact]
public void AuctionInfo_AddLogTyped_AddsWithLevelAndCategory()
{
var auction = new AuctionInfo { AuctionId = "1" };
auction.AddLog("Test", AuctionLogLevel.Warning, AuctionLogCategory.Price);
Assert.Single(auction.AuctionLog);
Assert.Equal(AuctionLogLevel.Warning, auction.AuctionLog[0].Level);
Assert.Equal(AuctionLogCategory.Price, auction.AuctionLog[0].Category);
}
// === AuctionInfo.AddLatencyMeasurement ===
[Fact]
public void AuctionInfo_AddLatencyMeasurement_AddsToHistory()
{
var auction = new AuctionInfo { AuctionId = "1" };
auction.AddLatencyMeasurement(50);
auction.AddLatencyMeasurement(100);
Assert.Equal(2, auction.LatencyHistory.Count);
Assert.Equal(100, auction.PollingLatencyMs);
}
[Fact]
public void AuctionInfo_AddLatencyMeasurement_TruncatesHistory()
{
var auction = new AuctionInfo { AuctionId = "1" };
for (int i = 0; i < 20; i++)
{
auction.AddLatencyMeasurement(i * 10);
}
Assert.True(auction.LatencyHistory.Count <= 10);
}
[Fact]
public void AuctionInfo_AverageLatencyMs_ComputesAverage()
{
var auction = new AuctionInfo { AuctionId = "1" };
auction.AddLatencyMeasurement(50);
auction.AddLatencyMeasurement(100);
Assert.Equal(75.0, auction.AverageLatencyMs, 1);
}
[Fact]
public void AuctionInfo_AverageLatencyMs_FallsBackToPollingLatency()
{
var auction = new AuctionInfo { AuctionId = "1", PollingLatencyMs = 80 };
Assert.Equal(80.0, auction.AverageLatencyMs, 1);
}
[Fact]
public void AuctionInfo_AverageLatencyMs_Default60WhenNoData()
{
var auction = new AuctionInfo { AuctionId = "1" };
Assert.Equal(60.0, auction.AverageLatencyMs, 1);
}
}
@@ -0,0 +1,161 @@
using AutoBidder.Models;
using Xunit;
namespace AutoBidder.Tests;
public class ProductInsightsTests
{
// === Calculate ===
[Fact]
public void Calculate_NullAuctions_ReturnsLowConfidence()
{
var insights = ProductInsights.Calculate(null!, "key", "name");
Assert.Equal(0, insights.ConfidenceScore);
Assert.Contains("insufficienti", insights.RecommendedStrategy);
}
[Fact]
public void Calculate_EmptyAuctions_ReturnsLowConfidence()
{
var insights = ProductInsights.Calculate(new List<ClosedAuctionRecord>(), "key", "name");
Assert.Equal(0, insights.ConfidenceScore);
}
[Fact]
public void Calculate_WithValidData_ComputesPriceStats()
{
var auctions = new List<ClosedAuctionRecord>
{
new() { FinalPrice = 5.0, BidsUsed = 10, ScrapedAt = DateTime.UtcNow },
new() { FinalPrice = 10.0, BidsUsed = 20, ScrapedAt = DateTime.UtcNow },
new() { FinalPrice = 15.0, BidsUsed = 30, ScrapedAt = DateTime.UtcNow },
};
var insights = ProductInsights.Calculate(auctions, "key", "Test Product");
Assert.Equal(3, insights.TotalAuctions);
Assert.Equal(10.0, insights.AverageFinalPrice, 1);
Assert.Equal(5.0, insights.MinFinalPrice, 1);
Assert.Equal(15.0, insights.MaxFinalPrice, 1);
Assert.Equal(10.0, insights.MedianFinalPrice, 1);
}
[Fact]
public void Calculate_WithBidsData_ComputesBidsStats()
{
var auctions = new List<ClosedAuctionRecord>
{
new() { FinalPrice = 5.0, BidsUsed = 10, ScrapedAt = DateTime.UtcNow },
new() { FinalPrice = 10.0, BidsUsed = 20, ScrapedAt = DateTime.UtcNow },
new() { FinalPrice = 15.0, BidsUsed = 30, ScrapedAt = DateTime.UtcNow },
};
var insights = ProductInsights.Calculate(auctions, "key", "name");
Assert.Equal(20.0, insights.AverageBidsUsed, 1);
Assert.Equal(10, insights.MinBidsUsed);
Assert.Equal(30, insights.MaxBidsUsed);
}
[Fact]
public void Calculate_SetsRecommendedValues()
{
var auctions = Enumerable.Range(1, 10)
.Select(i => new ClosedAuctionRecord
{
FinalPrice = i * 2.0,
BidsUsed = i * 5,
ScrapedAt = DateTime.UtcNow
})
.ToList();
var insights = ProductInsights.Calculate(auctions, "key", "name");
Assert.True(insights.RecommendedMaxPrice > 0);
Assert.True(insights.RecommendedMaxBids > 0);
Assert.False(string.IsNullOrEmpty(insights.RecommendedStrategy));
}
[Fact]
public void Calculate_HourlyDistribution_BasedOnScrapedAt()
{
var auctions = new List<ClosedAuctionRecord>
{
new() { FinalPrice = 5.0, ScrapedAt = new DateTime(2024, 1, 1, 14, 0, 0) },
new() { FinalPrice = 10.0, ScrapedAt = new DateTime(2024, 1, 1, 14, 30, 0) },
new() { FinalPrice = 15.0, ScrapedAt = new DateTime(2024, 1, 1, 20, 0, 0) },
};
var insights = ProductInsights.Calculate(auctions, "key", "name");
Assert.True(insights.HourlyDistribution.ContainsKey(14));
Assert.Equal(2, insights.HourlyDistribution[14]);
}
[Fact]
public void Calculate_OptimalStartPrice_NonNegative()
{
var auctions = new List<ClosedAuctionRecord>
{
new() { FinalPrice = 1.0, ScrapedAt = DateTime.UtcNow },
new() { FinalPrice = 2.0, ScrapedAt = DateTime.UtcNow },
};
var insights = ProductInsights.Calculate(auctions, "key", "name");
Assert.True(insights.OptimalStartPrice >= 0);
}
[Fact]
public void Calculate_HighCompetition_StrategyContainsAlta()
{
// Create data with high bid variance relative to average (high competition intensity)
var auctions = new List<ClosedAuctionRecord>
{
new() { FinalPrice = 5.0, BidsUsed = 100, ScrapedAt = DateTime.UtcNow },
new() { FinalPrice = 10.0, BidsUsed = 5, ScrapedAt = DateTime.UtcNow },
new() { FinalPrice = 15.0, BidsUsed = 200, ScrapedAt = DateTime.UtcNow },
new() { FinalPrice = 8.0, BidsUsed = 3, ScrapedAt = DateTime.UtcNow },
new() { FinalPrice = 12.0, BidsUsed = 150, ScrapedAt = DateTime.UtcNow },
};
var insights = ProductInsights.Calculate(auctions, "key", "name");
// CompetitionIntensity is clamped to [0,1]; just verify it's calculated
Assert.True(insights.CompetitionIntensity >= 0);
Assert.True(insights.CompetitionIntensity <= 1);
}
[Fact]
public void Calculate_LowConfidence_StrategyContainsWarning()
{
// Very few samples = low confidence
var auctions = new List<ClosedAuctionRecord>
{
new() { FinalPrice = 5.0, BidsUsed = 10, ScrapedAt = DateTime.UtcNow },
};
var insights = ProductInsights.Calculate(auctions, "key", "name");
Assert.True(insights.ConfidenceScore < 50);
}
[Fact]
public void Calculate_NullPricesAndBids_HandlesGracefully()
{
var auctions = new List<ClosedAuctionRecord>
{
new() { FinalPrice = null, BidsUsed = null, ScrapedAt = DateTime.UtcNow },
new() { FinalPrice = null, BidsUsed = null, ScrapedAt = DateTime.UtcNow },
};
var insights = ProductInsights.Calculate(auctions, "key", "name");
Assert.Equal(2, insights.TotalAuctions);
Assert.Equal(0, insights.AverageFinalPrice);
Assert.Equal(0, insights.AverageBidsUsed);
}
}
@@ -0,0 +1,188 @@
using AutoBidder.Models;
using AutoBidder.Services;
using Xunit;
namespace AutoBidder.Tests;
public class ProductStatisticsServiceTests
{
// === GenerateProductKey ===
[Fact]
public void GenerateProductKey_NullOrWhitespace_ReturnsUnknown()
{
Assert.Equal("unknown", ProductStatisticsService.GenerateProductKey(null!));
Assert.Equal("unknown", ProductStatisticsService.GenerateProductKey(""));
Assert.Equal("unknown", ProductStatisticsService.GenerateProductKey(" "));
}
[Fact]
public void GenerateProductKey_NormalizesToLowerAndTrims()
{
var key = ProductStatisticsService.GenerateProductKey(" iPhone 15 Pro ");
Assert.DoesNotContain(" ", key);
Assert.Equal(key, key.ToLowerInvariant());
}
[Fact]
public void GenerateProductKey_RemovesParentheses()
{
var key = ProductStatisticsService.GenerateProductKey("iPhone 15 (128GB Nero)");
Assert.DoesNotContain("128", key);
Assert.DoesNotContain("nero", key.ToLowerInvariant());
}
[Fact]
public void GenerateProductKey_RemovesBrackets()
{
var key = ProductStatisticsService.GenerateProductKey("Samsung Galaxy [Special Edition]");
Assert.DoesNotContain("special", key);
}
[Fact]
public void GenerateProductKey_RemovesColors()
{
var keyNero = ProductStatisticsService.GenerateProductKey("iPhone nero");
var keyBianco = ProductStatisticsService.GenerateProductKey("iPhone bianco");
// Both should produce same base key after color removal
Assert.Equal(keyNero, keyBianco);
}
[Fact]
public void GenerateProductKey_RemovesStorageCapacity()
{
var key128 = ProductStatisticsService.GenerateProductKey("iPhone 15 128GB");
var key256 = ProductStatisticsService.GenerateProductKey("iPhone 15 256GB");
Assert.Equal(key128, key256);
}
[Fact]
public void GenerateProductKey_LimitsLengthTo50()
{
var longName = new string('a', 200);
var key = ProductStatisticsService.GenerateProductKey(longName);
Assert.True(key.Length <= 50);
}
[Fact]
public void GenerateProductKey_SameProductDifferentVariants_SameKey()
{
var key1 = ProductStatisticsService.GenerateProductKey("Apple iPhone 15 Pro");
var key2 = ProductStatisticsService.GenerateProductKey("Apple iPhone 15 Pro (256GB)");
Assert.Equal(key1, key2);
}
// === CalculateRecommendedLimits ===
[Fact]
public void CalculateRecommendedLimits_LessThan3Results_ZeroConfidence()
{
var service = CreateService();
var results = new List<AuctionResultExtended>
{
CreateResult(won: true, price: 5.0)
};
var limits = service.CalculateRecommendedLimits(results);
Assert.Equal(0, limits.ConfidenceScore);
}
[Fact]
public void CalculateRecommendedLimits_NoWins_ReturnsConservativeLimits()
{
var service = CreateService();
var results = Enumerable.Range(1, 5)
.Select(i => CreateResult(won: false, price: 10.0 + i))
.ToList();
var limits = service.CalculateRecommendedLimits(results);
Assert.Equal(10, limits.ConfidenceScore);
Assert.True(limits.MinPrice > 0);
Assert.True(limits.MaxPrice > limits.MinPrice);
}
[Fact]
public void CalculateRecommendedLimits_WithWins_CalculatesPercentilePrices()
{
var service = CreateService();
var results = Enumerable.Range(1, 20)
.Select(i => CreateResult(won: true, price: i * 1.0, resets: i, bidsUsed: i * 5))
.ToList();
var limits = service.CalculateRecommendedLimits(results);
Assert.True(limits.MinPrice > 0);
Assert.True(limits.MaxPrice > limits.MinPrice);
Assert.True(limits.MaxBids > 0);
Assert.True(limits.ConfidenceScore >= 50);
}
[Fact]
public void CalculateRecommendedLimits_ConfidenceScalesWithSampleSize()
{
var service = CreateService();
var small = Enumerable.Range(1, 5)
.Select(i => CreateResult(won: true, price: i * 1.0))
.ToList();
var large = Enumerable.Range(1, 50)
.Select(i => CreateResult(won: true, price: i * 1.0))
.ToList();
var smallLimits = service.CalculateRecommendedLimits(small);
var largeLimits = service.CalculateRecommendedLimits(large);
Assert.True(largeLimits.ConfidenceScore > smallLimits.ConfidenceScore);
}
[Fact]
public void CalculateRecommendedLimits_FindsBestHourToPlay()
{
var service = CreateService();
var results = new List<AuctionResultExtended>();
// 5 wins at hour 14, 2 wins at hour 20
for (int i = 0; i < 5; i++)
results.Add(CreateResult(won: true, price: 5.0, closedAtHour: 14));
for (int i = 0; i < 2; i++)
results.Add(CreateResult(won: true, price: 5.0, closedAtHour: 20));
var limits = service.CalculateRecommendedLimits(results);
Assert.Equal(14, limits.BestHourToPlay);
}
// === Helpers ===
private static ProductStatisticsService CreateService()
{
// ProductStatisticsService requires DatabaseService, but CalculateRecommendedLimits
// and GenerateProductKey don't use it. We pass null through a helper constructor workaround.
// Since there's no interface, we instantiate with a real (unused) DatabaseService.
// The static methods and CalculateRecommendedLimits don't access _db.
return new ProductStatisticsService(null!);
}
private static AuctionResultExtended CreateResult(
bool won,
double price,
int? resets = null,
int bidsUsed = 0,
int? winnerBidsUsed = null,
int? closedAtHour = null)
{
return new AuctionResultExtended
{
AuctionId = Guid.NewGuid().ToString(),
AuctionName = "Test",
FinalPrice = price,
BidsUsed = bidsUsed,
Won = won,
TotalResets = resets,
WinnerBidsUsed = winnerBidsUsed ?? (won ? bidsUsed : null),
ClosedAtHour = closedAtHour
};
}
}
@@ -0,0 +1,285 @@
using AutoBidder.Models;
using AutoBidder.Utilities;
using Xunit;
namespace AutoBidder.Tests;
public class ProductValueCalculatorTests
{
// === Calculate ===
[Fact]
public void Calculate_BasicAuction_ComputesTotalCostIfWin()
{
var auction = new AuctionInfo
{
AuctionId = "1",
BidCost = 0.20,
BuyNowPrice = 100.0,
ShippingCost = 5.0,
BidsUsedOnThisAuction = 10
};
var result = ProductValueCalculator.Calculate(auction, currentPrice: 2.0, totalBids: 200);
Assert.Equal(2.0, result.CurrentPrice);
Assert.Equal(200, result.TotalBids);
Assert.Equal(10, result.MyBids);
Assert.Equal(2.0, result.MyBidsCost, 2); // 10 * 0.20
Assert.Equal(9.0, result.TotalCostIfWin, 2); // 2.0 + 2.0 + 5.0
}
[Fact]
public void Calculate_WithBuyNowPrice_ComputesSavings()
{
var auction = new AuctionInfo
{
AuctionId = "1",
BidCost = 0.20,
BuyNowPrice = 100.0,
ShippingCost = 5.0,
BidsUsedOnThisAuction = 10
};
var result = ProductValueCalculator.Calculate(auction, currentPrice: 2.0, totalBids: 200);
// buyNowTotal = 100 + 5 = 105
// totalCostIfWin = 2.0 + 2.0 + 5.0 = 9.0
// savings = 105 - 9 = 96
Assert.True(result.Savings.HasValue);
Assert.Equal(96.0, result.Savings!.Value, 2);
Assert.True(result.IsWorthIt);
}
[Fact]
public void Calculate_WithoutBuyNowPrice_IsWorthItTrue()
{
var auction = new AuctionInfo
{
AuctionId = "1",
BidCost = 0.20,
BuyNowPrice = null,
ShippingCost = null,
BidsUsedOnThisAuction = 5
};
var result = ProductValueCalculator.Calculate(auction, currentPrice: 1.0, totalBids: 100);
Assert.True(result.IsWorthIt);
Assert.Null(result.Savings);
Assert.Null(result.SavingsPercentage);
}
[Fact]
public void Calculate_WhenTotalCostExceedsBuyNow_NotWorthIt()
{
var auction = new AuctionInfo
{
AuctionId = "1",
BidCost = 0.20,
BuyNowPrice = 5.0,
ShippingCost = 1.0,
BidsUsedOnThisAuction = 50
};
// totalCostIfWin = 3.0 + 10.0 + 1.0 = 14.0 > buyNowTotal (5 + 1 = 6)
var result = ProductValueCalculator.Calculate(auction, currentPrice: 3.0, totalBids: 300);
Assert.False(result.IsWorthIt);
Assert.True(result.Savings!.Value < 0);
}
[Fact]
public void Calculate_ZeroBuyNowPrice_IsWorthItTrue()
{
var auction = new AuctionInfo
{
AuctionId = "1",
BidCost = 0.20,
BuyNowPrice = 0.0,
ShippingCost = null,
BidsUsedOnThisAuction = 1
};
var result = ProductValueCalculator.Calculate(auction, currentPrice: 1.0, totalBids: 100);
// BuyNowPrice == 0 falls into the else branch (no valid buy now price)
Assert.True(result.IsWorthIt);
Assert.Null(result.Savings);
Assert.Null(result.SavingsPercentage);
}
[Fact]
public void Calculate_NoShippingCost_ExcludesShippingFromTotal()
{
var auction = new AuctionInfo
{
AuctionId = "1",
BidCost = 0.20,
BuyNowPrice = 50.0,
ShippingCost = null,
BidsUsedOnThisAuction = 5
};
var result = ProductValueCalculator.Calculate(auction, currentPrice: 1.0, totalBids: 100);
// totalCostIfWin = 1.0 + 1.0 = 2.0 (no shipping)
Assert.Equal(2.0, result.TotalCostIfWin, 2);
}
[Fact]
public void Calculate_GeneratesSummaryString()
{
var auction = new AuctionInfo
{
AuctionId = "1",
BidCost = 0.20,
BuyNowPrice = 100.0,
ShippingCost = null,
BidsUsedOnThisAuction = 0
};
var result = ProductValueCalculator.Calculate(auction, currentPrice: 5.0, totalBids: 500);
Assert.False(string.IsNullOrEmpty(result.Summary));
}
// === ExtractProductInfo ===
[Fact]
public void ExtractProductInfo_EmptyHtml_ReturnsFalse()
{
var auction = new AuctionInfo { AuctionId = "1" };
Assert.False(ProductValueCalculator.ExtractProductInfo("", auction));
Assert.False(ProductValueCalculator.ExtractProductInfo(null!, auction));
}
[Fact]
public void ExtractProductInfo_ProductValueSpan_ExtractsBuyNowPrice()
{
var html = @"<span class=""text-muted product-value""><span class=""hidden-xs"">Valore: </span><span class=""product-value hidden-xs"">18,90 €</span></span>";
var auction = new AuctionInfo { AuctionId = "1" };
var result = ProductValueCalculator.ExtractProductInfo(html, auction);
Assert.True(result);
Assert.Equal(18.90, auction.BuyNowPrice!.Value, 2);
}
[Fact]
public void ExtractProductInfo_BuyItNowButton_ExtractsBuyNowPrice()
{
var html = @"<span class=""buyitnow-button label"">Compra a 25,50 €</span>";
var auction = new AuctionInfo { AuctionId = "1" };
var result = ProductValueCalculator.ExtractProductInfo(html, auction);
Assert.True(result);
Assert.Equal(25.50, auction.BuyNowPrice!.Value, 2);
}
[Fact]
public void ExtractProductInfo_CompraloOra_ExtractsBuyNowPrice()
{
var html = @"COMPRALO ORA A 42,99 €";
var auction = new AuctionInfo { AuctionId = "1" };
var result = ProductValueCalculator.ExtractProductInfo(html, auction);
Assert.True(result);
Assert.Equal(42.99, auction.BuyNowPrice!.Value, 2);
}
[Fact]
public void ExtractProductInfo_ShippingCost_ExtractsShipping()
{
var html = @"<strong class=""mobile-left-truck"">Spese di spedizione:</strong> <span class=""text-success"">4,99 €</span>";
var auction = new AuctionInfo { AuctionId = "1" };
var result = ProductValueCalculator.ExtractProductInfo(html, auction);
Assert.True(result);
Assert.Equal(4.99, auction.ShippingCost!.Value, 2);
}
[Fact]
public void ExtractProductInfo_TransactionCost_ExtractsAsShipping()
{
var html = @"<strong>Spese di transazione:</strong> <span class=""text-success"">1,00 €</span>";
var auction = new AuctionInfo { AuctionId = "1" };
var result = ProductValueCalculator.ExtractProductInfo(html, auction);
Assert.True(result);
Assert.Equal(1.00, auction.ShippingCost!.Value, 2);
}
[Fact]
public void ExtractProductInfo_WinLimit_ExtractsLimit()
{
var html = @"Limiti di vincita: <span class=""limit-info"">1 ogni 30 giorni</span>";
var auction = new AuctionInfo { AuctionId = "1" };
var result = ProductValueCalculator.ExtractProductInfo(html, auction);
Assert.True(result);
Assert.True(auction.HasWinLimit);
Assert.Equal("1 ogni 30 giorni", auction.WinLimitDescription);
}
// === FormatValueMessage ===
[Fact]
public void FormatValueMessage_NoBuyNowPrice_ReturnsGenericMessage()
{
var value = new ProductValue
{
TotalCostIfWin = 5.0,
CurrentPrice = 3.0,
MyBidsCost = 2.0,
BuyNowPrice = null
};
var message = ProductValueCalculator.FormatValueMessage(value);
Assert.Contains("5,00", message);
}
[Fact]
public void FormatValueMessage_IsWorthIt_ContainsConveniente()
{
var value = new ProductValue
{
TotalCostIfWin = 10.0,
CurrentPrice = 5.0,
MyBidsCost = 5.0,
BuyNowPrice = 100.0,
IsWorthIt = true,
Savings = 90.0,
SavingsPercentage = 90.0
};
var message = ProductValueCalculator.FormatValueMessage(value);
Assert.Contains("Conveniente", message);
}
[Fact]
public void FormatValueMessage_NotWorthIt_ContainsNonConveniente()
{
var value = new ProductValue
{
TotalCostIfWin = 120.0,
CurrentPrice = 100.0,
MyBidsCost = 20.0,
BuyNowPrice = 50.0,
IsWorthIt = false,
Savings = -70.0,
SavingsPercentage = -140.0
};
var message = ProductValueCalculator.FormatValueMessage(value);
Assert.Contains("Non conveniente", message);
}
}
+10
View File
@@ -429,6 +429,16 @@ namespace AutoBidder.Utilities
/// Default: true /// Default: true
/// </summary> /// </summary>
public bool SaveBidMetricsToDatabase { get; set; } = true; public bool SaveBidMetricsToDatabase { get; set; } = true;
// ═══════════════════════════════════════════════════════════════════
// INTERFACCIA
// ═══════════════════════════════════════════════════════════════════
/// <summary>
/// Abilita la modalità scura dell'interfaccia.
/// Default: false (modalità chiara)
/// </summary>
public bool DarkMode { get; set; } = false;
} }
public static class SettingsManager public static class SettingsManager
File diff suppressed because it is too large Load Diff