Files
Mimante/Mimante/Utilities/ClaimedPromoStore.cs
T
Alby96andClaude Fable 5.1 9177b31bd5 Due database, interruttore dell'apprendimento, barre a icone; via kill-switch, esportazioni e voci deprecate
Tutto ciò che l'applicazione registra sta ora in due file SQLite in
%LocalAppData%\AutoBidder\Database: autobidder.sqlite per le osservazioni e
esercizio.sqlite per i documenti d'esercizio (aste nel monitor, prodotti, promozioni,
modelli appresi con prefisso ml/) e per i registri applicativo e del riscatto puntate.
Il motore comune è SqliteDatabase; i file JSON e la cartella Apprendimento delle
versioni precedenti vengono importati la prima volta e lasciati dove sono. La cartella
si cambia dalle Impostazioni: al salvataggio si chiede se spostare i file, e si riavvia.
Le cartelle Dati/Statistiche/Registri, i registri su file, le misure dell'anticipo e
le impostazioni morte (LogBids, AutoApplyProductDefaults, NewAuctionLimitsPriority…)
non esistono più.

Il kill-switch se ne va: al suo posto un solo interruttore in barra, accanto ad
Avvia/Osserva/Ferma, che spegne l'apprendimento. Spento, il motore punta solo entro i
limiti dell'utente (prezzo, puntate, budget, fascia oraria, rischio) con l'anticipo
fisso: niente valore atteso, regime, duello, bandit, anticipo adattivo. Le aste vengono
comunque registrate e studiate.

Tre difetti visti osservando il motore dal vivo per un'ora, in Osserva:
- la copertura «Compralo Ora» con V lasciato al listino azzerava la perdita coperta e
  il motore diceva BID a ogni scadenza con P dell'1%: ora vale solo per i prodotti con
  «Valore reale €» scritto, altrimenti la puntata si conta persa e P decide;
- la chiusura di un'asta arrivava due volte (fine, poi rimozione dal monitor) e di
  nuovo al riavvio: modello, profilo, bandit e statistiche per prodotto contavano ogni
  asta due volte. Il monitor la comunica una volta sola, l'apprendimento salta le aste
  già apprese, e le schede prodotto vengono ricostruite una volta dallo storico;
- la pagina delle ricompense veniva scambiata per la pagina di accesso perché porta un
  collegamento a /login.php: prima si guarda se è la pagina delle ricompense.

Interfaccia: la scheda Esporta e tutto il suo codice sono tolti; le esportazioni
chiedono sempre dove salvare. Prodotti, Storico e Apprendimento hanno barre a sole
icone con tooltip che dicono esattamente cosa succede, colorate come il monitor, con
i nuovi pulsanti di pulizia (spegni tutte le stelline, togli i non seguiti, pulizia
completa; seleziona tutte / elimina le selezionate / svuota lo storico). Impostazioni:
sezione Database, Manutenzione con «Elimina tutti i dati (tranne la login)» e
«Impostazioni di fabbrica», esportazione dei registri in testo. Modifiche.txt, il
prompt ormai realizzato, esce dal repository.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 13:58:08 +02:00

157 lines
5.3 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
namespace AutoBidder.Utilities
{
/// <summary>
/// Ricorda quali collegamenti promozionali sono già stati aperti.
///
/// <para>È la parte che rende sensata la raccolta automatica. La pagina di origine tiene
/// gli stessi collegamenti per giorni: senza memoria, ogni giro riaprirebbe tutto —
/// decine di richieste identiche allo stesso indirizzo, per premi già presi. Con la
/// memoria, dopo il primo giro il lavoro normale è "trovati venti, nessuno nuovo".</para>
///
/// <para>Un codice fallito viene ritentato un numero limitato di volte: un collegamento
/// scaduto resta pubblicato per sempre, e ritentarlo all'infinito sarebbe lo stesso
/// spreco da cui questa classe difende.</para>
/// </summary>
public static class ClaimedPromoStore
{
/// <summary>Oltre questa soglia i codici più vecchi vengono dimenticati.</summary>
private const int MaxRecords = 3000;
private static readonly object Sync = new();
private static StoreData? _cache;
public const string DocumentKey = "claimed-promos";
public sealed class PromoRecord
{
public string Code { get; set; } = "";
public bool Claimed { get; set; }
public int Attempts { get; set; }
public DateTime FirstSeen { get; set; } = DateTime.Now;
public DateTime LastAttempt { get; set; } = DateTime.Now;
public string LastDetail { get; set; } = "";
}
private sealed class StoreData
{
public List<PromoRecord> Records { get; set; } = new();
}
/// <summary>
/// True se vale la pena provare questo codice: mai preso e non ancora ritentato
/// troppe volte.
/// </summary>
public static bool ShouldTry(string code, int maxAttempts)
{
if (string.IsNullOrWhiteSpace(code)) return false;
lock (Sync)
{
var record = Find(Load(), code);
if (record == null) return true;
if (record.Claimed) return false;
return record.Attempts < Math.Max(1, maxAttempts);
}
}
public static void RecordOutcome(string code, bool claimed, string detail)
{
if (string.IsNullOrWhiteSpace(code)) return;
lock (Sync)
{
var data = Load();
var record = Find(data, code);
if (record == null)
{
record = new PromoRecord { Code = code.Trim() };
data.Records.Add(record);
}
record.Claimed |= claimed;
record.Attempts++;
record.LastAttempt = DateTime.Now;
record.LastDetail = detail.Length <= 200 ? detail : detail[..200];
if (data.Records.Count > MaxRecords)
data.Records.RemoveRange(0, data.Records.Count - MaxRecords);
Save();
}
}
/// <summary>Quanti codici sono stati riscossi con successo, da sempre.</summary>
public static int ClaimedCount
{
get { lock (Sync) return Load().Records.Count(r => r.Claimed); }
}
/// <summary>Gli ultimi codici trattati, dal più recente. Per la scheda Puntate.</summary>
public static IReadOnlyList<PromoRecord> Recent(int count)
{
lock (Sync)
{
return Load().Records
.OrderByDescending(r => r.LastAttempt)
.Take(Math.Max(1, count))
.ToList();
}
}
/// <summary>
/// Dimentica tutto. Utile quando si cambia account: i codici presi da un utente non
/// dicono nulla su quelli ancora disponibili per un altro.
/// </summary>
public static void Clear()
{
lock (Sync)
{
_cache = new StoreData();
Save();
}
}
private static PromoRecord? Find(StoreData data, string code) =>
data.Records.FirstOrDefault(r => string.Equals(r.Code, code.Trim(), StringComparison.OrdinalIgnoreCase));
private static StoreData Load()
{
if (_cache != null) return _cache;
try
{
var json = Data.OperationalDatabase.Instance.GetDocumentOrImport(DocumentKey, AppPaths.LegacyClaimedPromosFile);
_cache = string.IsNullOrWhiteSpace(json)
? new StoreData()
: JsonSerializer.Deserialize<StoreData>(json) ?? new StoreData();
}
catch
{
// Un file illeggibile non deve fermare il riscatto: al massimo si riprova un
// codice già preso, che il sito rifiuterà da sé.
_cache = new StoreData();
}
return _cache;
}
private static void Save()
{
try
{
Data.OperationalDatabase.Instance.SetDocument(DocumentKey, JsonSerializer.Serialize(_cache));
}
catch { /* la persistenza non deve mai bloccare l'interfaccia */ }
}
}
}