# Porting Notes — AIUsage.NET (porting Windows di OpenUsage) Questo file traccia **ogni scelta di design, semplificazione, omissione o divergenza** rispetto all'edizione Swift/macOS originale (`openusage/`, progetto "OpenUsage" di Robin Ebers). Scopo: permettere di riprendere il lavoro in futuro sapendo esattamente cosa è stato adattato e perché, e cosa resta da fare per arrivare alla parità funzionale completa. ## Rebranding: OpenUsage → AIUsage.NET Il progetto è stato rinominato da "OpenUsage" ad **AIUsage.NET** (repo: https://github.com/LuigiElleBalotta/AIUsage.NET) per non presentarsi come un fork/copia sotto lo stesso nome. Di conseguenza: - Cartella radice: `openusageWindows/` → `AIUsage.NET/` - Solution: `OpenUsage.sln` → `AIUsage.sln` - Progetti: `OpenUsage.Core` → `AIUsage.Core`, `OpenUsage.Tray` → `AIUsage.Tray`, `OpenUsage.Cli` → `AIUsage.Cli` (namespace, `AssemblyName`, cartelle, file `.csproj` tutti rinominati in coerenza) - Eseguibili risultanti: `AIUsage.exe` (tray), `aiusage.exe` (CLI, minuscolo come l'originale `openusage`) - File/tipi rinominati: `OpenUsageISO8601` → `AIUsageISO8601` - Path di configurazione utente: `~/.openusage/config.json` → `~/.aiusage/config.json`; cache pricing locale: `%LOCALAPPDATA%\OpenUsage\pricing` → `%LOCALAPPDATA%\AIUsage\pricing` - Windows Credential Manager target prefix: `OpenUsage:` → `AIUsage:` - **Logo/icona**: un'icona `.ico` reale generata proceduralmente (vedi sezione UI WPF più sotto, `tools/IconGen/`) sostituisce ora il placeholder disegnato a runtime — non riusa nulla dell'originale, coerente con la richiesta esplicita dell'utente. `[DIVERGENTE — deliberato]` **Il fetch live del pricing supplement resta puntato a `https://robinebers.github.io/openusage/pricing_supplement.json`** (in `ModelPricingStore.cs`). Scelta esplicita dell'utente: i dati di pricing (aliases, fast-multiplier, modelli Cursor-nativi) non sono un problema di "copiare il brand", sono dati tecnici pubblici aggiornati da un progetto terzo e va bene continuare a consumarli così. Da rivalutare solo se in futuro si vuole rendere AIUsage.NET completamente autonomo anche per i dati. Il file bundlato `Resources/pricing_supplement.json` resta comunque una copia locale (fallback offline) dei dati di quello stesso progetto — stesso discorso, nessuna azione richiesta. Convenzione: ogni voce ha un tag di stato: - `[SEMPLIFICATO]` — logica portata ma ridotta rispetto all'originale (di solito perché l'infra macOS-specifica non ha un equivalente diretto o non è essenziale per la correttezza). - `[OMESSO]` — funzionalità non ancora implementata, da fare. - `[DIVERGENTE]` — scelta implementativa diversa per necessità della piattaforma Windows. - `[FEDELE]` — logica di business portata 1:1 (stessa formula/algoritmo), solo sintassi C#. --- ## Stack e struttura progetto - **Linguaggio/runtime**: .NET 8, scelto perché già installato e perché C#/WPF è l'equivalente più diretto di Swift/AppKit per un'app "tray" su Windows. - **UI**: WPF (non ancora implementata al momento di questa nota — solo lo scaffold `OpenUsage.Tray` esiste). `NSStatusItem` → `System.Windows.Forms.NotifyIcon` (o libreria tray WPF); `NSPanel` popover → finestra WPF senza chrome, posizionata vicino alla tray icon. - Struttura a 3 progetti che rispecchia l'originale: - `OpenUsage.Core` ≈ `Sources/OpenUsage` (libreria condivisa: modelli, provider, pricing, servizi) - `OpenUsage.Tray` ≈ `Sources/OpenUsageApp` (eseguibile GUI, WPF) - `OpenUsage.Cli` ≈ `Sources/OpenUsageCLI` (eseguibile CLI `openusage`) - ~~`[OMESSO]` Nessun progetto di test ancora creato~~ — **fatto** in una sessione successiva: `tests/AIUsage.Core.Tests` con xUnit, 255 test (mapper di tutti i 10 provider + helper di supporto). Vedi la sezione "Test automatici" più sotto per il dettaglio. ## Credenziali: mapping macOS → Windows | Originale (macOS) | Windows | Stato | |---|---|---| | Keychain (`security` CLI) | Windows Credential Manager (`CredRead`/`CredWrite` via P/Invoke, `advapi32.dll`) | `[DIVERGENTE]` — vedi `Services/SystemClients.cs` → `WindowsCredentialAccessor`. Target name prefissato `OpenUsage:` per non collidere con credenziali di altre app. | | File config (`~/.claude/.credentials.json`, `~/.codex/auth.json`, ecc.) | Stessi file JSON, path espanso da `~` a `%USERPROFILE%` | `[FEDELE]` per i tool CLI cross-platform (Claude Code, Codex CLI, ecc. scrivono lo stesso file su ogni OS) | | SQLite (Cursor `state.vscdb`) | Stesso file, letto con `Microsoft.Data.Sqlite` invece del CLI `sqlite3` | `[DIVERGENTE]` — vedi `SqliteDataAccessor`. Nessun processo esterno necessario. | | File con permessi 0600 (scrittura atomica) | Scrittura atomica (temp file + rename) + ACL NTFS ristretta all'utente corrente via `FileSystemAccessRule` | `[DIVERGENTE]` — vedi `LocalTextFileAccessor.WriteText` | | Login-shell environment capture (`LoginShellEnvironment`, per app lanciate da Finder/Dock) | Non necessario: un'app Windows lanciata da Explorer/Start Menu eredita già le env var utente/macchina persistite (`HKCU\Environment`) | `[OMESSO — non serve]` Nessuna cattura di shell necessaria; `ProcessEnvironmentReader` legge Process→User→Machine env vars direttamente. | `[OMESSO]` **Claude Desktop fallback**: l'originale ha `ClaudeDesktopAuthStore` per leggere le credenziali dell'app desktop Claude quando il CLI non è loggato. Non portato — non esiste un "Claude Desktop" equivalente rilevante da investigare su Windows separato dal CLI. Da valutare in futuro se Anthropic rilascia un client desktop Windows con storage credenziali proprio. ~~`[OMESSO]` **Multi-account Claude cards**~~ — **fatto**: porta quasi 1:1 l'intera pipeline Swift (`ProviderAccountID`, `DefaultAccountObserver`, `ClaudeConfigDirDiscovery`, `ProviderAccountsStore`, `ProviderAccountAssembly`), Claude-only (Codex multi-account resta scope Swift-originale, non richiesto per questo port — solo la famiglia `"claude"` è registrata in `ProviderAccountID.Families`). - **`Stores/ProviderAccountsStore.cs`**: registro persistente (`aiusage.providerAccounts.v1` via `ISettingsStore`, equivalente Windows di `UserDefaults`) di `ProviderAccountRecord` (id/family/ identityKey/label/customLabel/sources/removedTombstone). Il primo account trovato all'home di default eredita l'id "nudo" (`claude`, per compatibilità con installazioni esistenti — nessuna migrazione necessaria); ogni account successivo riceve `family@hash8` (`ProviderAccountID.Make`, SHA-256 del identity key troncato a 8 hex). `Reconcile()` fonde le osservazioni di ogni avvio senza mai far scomparire un record non osservato; il flag "default source" passa esclusivamente all'account che occupa davvero l'home di default in quel momento. `Rename()`/`ResolvedDisplayName()` sono l'unico punto di risoluzione del nome mostrato all'utente (un rename vince sempre sul nome derivato "Claude — Org"). - **`Providers/Claude/DefaultAccountObserver.cs`**: legge `~/.claude.json` (o `/.claude.json` — il file di stato Claude Code vive SEMPRE accanto alla, non dentro la, cartella di config di default) per estrarre `oauthAccount.accountUuid`/ `organizationUuid`/`emailAddress`/`organizationName`. Identity key = `"{uuid}|{orgUuid}"` (o solo `uuid` senza org) — i piani sono scoped per org, quindi un umano con un org Max personale e un org Team aziendale sotto lo stesso account devono restare due account distinti. Un `CLAUDE_CONFIG_DIR` con lista separata da virgole non può avere un'identità unica → `unresolved`. - **`Providers/Claude/ClaudeConfigDirDiscovery.cs`**: scansione al lancio (budget 400ms) di dot-dir sotto `~` e `~/.config`, esclusi gli home di default, in cerca di altri login Claude. Un candidato conta solo se ha sia un file d'identità (`.claude.json`) valido SIA una credenziale (file `.credentials.json` locale, oppure una voce Windows Credential Manager con nome calcolato — probing solo per esistenza degli attributi, mai lettura del segreto, per non far comparire mai un prompt di credenziali durante il probe di avvio). - **`Providers/Claude/ClaudeAuthStore.cs`**: aggiunto `ClaudeCredentialScope` (union type `Standard`/`ConfigDir(path, keychainLiteral)`) e `AllowsDesktopFallback`. Una card `ConfigDir` non legge MAI il file/keychain di default, non applica MAI il token d'ambiente `CLAUDE_CODE_OAUTH_TOKEN` (che descrive l'ambiente dell'account di DEFAULT, non della card scoped) e non tenta MAI il fallback Claude Desktop (un login Desktop non pinnato potrebbe appartenere a qualsiasi account). Aggiunti `ScopedKeychainServiceName`/`BaseKeychainServiceName` (helper statici usati sia da questo store che da `ClaudeConfigDirDiscovery`, per garantire che probing e lettura calcolino ESATTAMENTE lo stesso hash) e l'algoritmo `HashSuffix` — porto letterale dello Swift `hashSuffix` privato: SHA-256 del literal normalizzato NFC, hex, primi 8 caratteri. - **`Providers/Claude/ClaudeLogUsageScanner.cs`**: aggiunti `fixedRoots` (una card `ConfigDir` legge SOLO la sua cartella di log, mai quella di default) e `extraRoots` (la card di default, quando la discovery trova altre cartelle con lo STESSO account, le aggiunge come radici extra dei log di spesa — mai come credenziali extra — senza sostituire la risoluzione normale env/home). - **`App/ProviderAccountAssembly.cs`**: l'orchestratore a livello di avvio — osserva l'account di default, esegue la discovery, riconcilia via `ProviderAccountsStore`, ed espone `IdentityKeysByCard` + `ClaudeCards` (record `ClaudeAccountCard`: id/displayName/configDirPath/keychainLiteral/ extraLogRoots) che `ProviderCatalog.Make()` consuma per costruire istanze `ClaudeProvider` extra. **Omessa deliberatamente** la logica `shellFactsReadable`/login-shell-cold-launch dell'originale Swift: esiste solo perché le app macOS lanciate da Finder/Dock non erediscono le env var esportate dalla shell — su Windows un processo lanciato da Explorer/Start Menu eredita sempre le env var utente/macchina persistite, quindi questa cautela non ha equivalente e la lettura dell'identità avviene sempre in modo sincrono e immediato. - **`Providers/ProviderCatalog.cs`**: `Make()` ora accetta un `ProviderAccountsStore?` opzionale, costruisce l'assembly, e produce la card Claude di default (con `AllowsDesktopFallback=false` non appena esiste almeno una card extra — un login Desktop non pinnato potrebbe appartenere a qualunque account) più una `ClaudeProvider.MakeAccountCard(...)` per ogni `ClaudeAccountCard` trovata, ciascuna con `Provider.Id` uguale al proprio record id (`claude@ab12cd34`, mai `claude`). - **UI WPF**: `MetricsWindow` risolve ora il nome di ogni card tramite `AppContainer.DisplayName(id)` (rename-aware) invece del vecchio `Provider.DisplayName` statico; le card con id "account" (`ProviderAccountID.IsAccountCard`) ricevono un menu contestuale "Rename..." sull'header che apre un nuovo `RenameDialog.xaml` minimale (textbox + Save/Cancel) e chiama `AppContainer.RenameProvider(id, name)` → `ProviderAccountsStore.Rename`. `SettingsWindow` usa la stessa risoluzione per l'elenco toggle provider e la lista Customize per-metrica. Le card extra compaiono automaticamente nella dashboard e nelle liste Settings perché entrambe iterano `Registry.Providers`/`Registry.DescriptorsFor(id)`, senza bisogno di logica speciale. - **Verificato** contro un'installazione reale Claude Code presente su questa macchina (singolo account, nessuna card extra attesa): `aiusage claude --force` restituisce dati corretti (piano "Pro", limiti sessione/settimanali) passando per la nuova pipeline `ProviderAccountAssembly` → `ProviderCatalog.Make()`, confermando che il caso a singolo account (la stragrande maggioranza degli utenti) non regredisce. La pipeline multi-account vera e propria (più cartelle `CLAUDE_CONFIG_DIR`, card extra, rename) è verificata con 60+ test automatici su file system/keychain/env finti (`ProviderAccountsStoreTests`, `DefaultAccountObserverTests`, `ClaudeConfigDirDiscoveryTests`, `ClaudeAuthStoreScopeTests`, `ProviderAccountAssemblyTests`) ma NON contro un secondo account Claude reale su questa macchina (non disponibile per il test). Test: 361 test totali passano (`dotnet test`), 0 avvisi/0 errori in `dotnet build AIUsage.sln`. ## Pricing engine (`Pricing/`) `[FEDELE]` Portato quasi 1:1: `ModelRates`, `PricingCatalog` (fuzzy matching byte-a-byte identico), `PricingSupplement`, `PricingCatalogCodecs` (formato compatto, parser LiteLLM/models.dev), `ModelPricingStore`. I 3 JSON bundle (`pricing_supplement.json`, `pricing_litellm_snapshot.json`, `pricing_models_dev_snapshot.json`) copiati identici in `OpenUsage.Core/Resources/`. `[DIVERGENTE]` `ModelPricingStore` in Swift è un `actor`; in C# è una classe con `lock` interni — stessa semantica (accesso serializzato), sintassi diversa. `[SEMPLIFICATO]` Caricamento risorse bundle: Swift usa `Bundle.module`; C# usa `Path.Combine(AppContext.BaseDirectory, "Resources", ...)`. Va verificato che il `.csproj` copi effettivamente `Resources/**` nell'output (impostato con `CopyToOutputDirectory=PreserveNewest` in `OpenUsage.Core.csproj`, da testare a build reale). ## Logging (`Support/AppLog.cs`, `LogRedaction.cs`) `[FEDELE]` `LogRedaction` è un port pressoché letterale delle regex Rust/Swift (JWT, API key, devin-session-token, JSON keys sensibili, `account=`). `[DIVERGENTE]` Path redaction: aggiunto pattern per path stile Windows (`C:\...`, `\\unc\...`) oltre ai path Unix originali (`/Users/...` ecc., mantenuti per compatibilità con dati importati). `[SEMPLIFICATO]` `AppLog`: niente `os.Logger`/Console.app equivalente strutturato — solo file di log + `Debug.WriteLine`. Da valutare: integrazione con `Microsoft.Extensions.Logging` o ETW se serve diagnostica avanzata in futuro. `[FEDELE]` Percorso del file di log: `%LOCALAPPDATA%\AIUsage\Logs\AIUsage.log` (equivalente Windows di `~/Library/Logs/OpenUsage/OpenUsage.log`), impostato da `AIUsage.Tray/App.xaml.cs` che chiama `AppLog.Bootstrap(path)` all'avvio. `[FEDELE]` **Log rotation** (`Support/LogFile.cs`): porta 1:1 la logica dell'originale `LogFile.swift` — cap 10MB (`LogFile.DefaultMaxBytes`), rotazione a singolo archivio (`AIUsage.log` → `AIUsage.1.log` + nuovo `AIUsage.log` vuoto) quando una scrittura supererebbe il cap, trim al lancio se un file già sovradimensionato viene trovato da una sessione precedente, e disabilitazione silenziosa del sink (mai un crash) se apertura/rotazione fallisce. `AppLog` ora delega la scrittura su file a `LogFile` invece dello precedente `File.AppendAllText` diretto senza cap. Copertura test in `tests/AIUsage.Core.Tests/Support/LogFileTests.cs` (scrittura base, rotazione per superamento cap, trim al lancio su file esistente sovradimensionato, creazione automatica della directory). ## HTTP client (`Services/HttpClientService.cs`) `[DIVERGENTE]` `URLSessionHTTPClient` (con delegate TLS loopback custom per il local API HTTPS) → `SystemHttpClient` basato su `HttpClient`/`HttpClientHandler`. Il local API server useremo HTTP semplice su loopback (niente TLS self-signed) quando implementato — da decidere se serve davvero HTTPS anche su Windows per il local API (`docs/local-http-api.md` dice loopback-only, quindi il rischio è minimo anche in chiaro). `[SEMPLIFICATO]` `ProxyConfig`: il proxy SOCKS5 non è supportato nativamente da `HttpClientHandler`/`WebProxy` su .NET (limite della piattaforma, non una scelta di design). HTTP e HTTPS CONNECT funzionano pienamente; SOCKS5 configurato viene tentato ma degradato. Da rivalutare con una libreria terza (es. `SocksSharp` o proxy runtime a parte) se un utente lo richiede. ## Modelli (`Models/`) `[FEDELE]` `MetricLine`, `MetricValue`, `WidgetData`, `WidgetDescriptor`, `ProviderSnapshot`, `DailyUsageSeries`, ecc. — stessa struttura dati, in C# come `record`/classi invece di `enum` associati Swift. La union-type `MetricLine` è modellata con una classe abstract + record annidati (pattern matching via `switch` su tipo). `[DIVERGENTE — bug fix nome]` In `MetricLine.cs`, il record `Badge` aveva originariamente un parametro `Text` che collideva col tipo annidato `MetricLine.Text` (errore di compilazione C# CS8866). Rinominato in `BadgeText`. Puramente un dettaglio di implementazione C#, nessun impatto sul comportamento. `[DIVERGENTE — bug fix nome]` In `WidgetData.MeterState.Level`, il parametro posizionale `Severity` collideva con la proprietà calcolata `Severity` della classe base. Rinominato in `LevelSeverity`. `[OMESSO]` Alcune proprietà di tooltip molto SwiftUI-specifiche in `WidgetData` (es. `unboundedValueTooltip`, `unboundedTooltip` con soglie di abbreviazione, `expiryTooltip` col formato "Resets expire in: ...") sono state semplificate o non ancora portate integralmente: la logica di business core (fraction, meterState, headline, valueText) è fedele; i tooltip più elaborati verranno rifiniti quando si costruirà la UI WPF e si vede cosa serve davvero mostrare. ## Providers — stato di avanzamento | Provider | Stato | Note | |---|---|---| | Claude | **Fatto** (auth store, usage client, mapper, log scanner, provider, Desktop fallback, multi-account) | `[SEMPLIFICATO]` niente Cowork sandbox scan (macOS-only, cartelle sessione dell'app desktop Claude) | | Codex | **Fatto** (auth store, usage client, mapper, log scanner, provider) | `[FEDELE]` logica di pricing/parsing rollout (child-session replay gate, service tier, long-context rates, auto-review fallback) portata 1:1. Keychain di sistema come fallback secondario (i file `auth.json` sotto `%USERPROFILE%\.codex` restano la fonte primaria, identica su ogni OS) | | Cursor | **Fatto** (auth store, CSV parser, usage client, mapper, summary mapper, provider) | `[DIVERGENTE]` `state.vscdb` letto con `Microsoft.Data.Sqlite` invece del CLI `sqlite3` — stesso file (equivalente Windows: `%APPDATA%\Cursor\User\globalStorage\state.vscdb`), stessa struttura, nessun processo esterno necessario | | Copilot | **Fatto** (auth store, usage client, mapper, org billing client/mapper, provider) | `[DIVERGENTE]` cache dell'org di billing: l'originale usa `UserDefaults`, qui un piccolo `FileSettingsStore` JSON in `%LOCALAPPDATA%\AIUsage\settings.json` (vedi `Services/SettingsStore.cs`) — stessa semantica chiave/valore | | Grok | **Fatto** (auth store, usage client, credits config decoder, log scanner, mapper, provider) | `[FEDELE]` | | Devin | **Fatto** (auth store, usage client, mapper, provider) | `[DIVERGENTE]` `state.vscdb` via `Microsoft.Data.Sqlite`; credentials.toml sotto `%LOCALAPPDATA%\devin\` (equivalente Windows di `~/.local/share/devin/`) | | OpenCode | **Fatto** (paths, auth store, Go window math, mapper, scanner, provider) | `[FEDELE]` la math delle finestre (session/weekly/monthly anchor) era già scritta cross-platform nell'originale (`OpenCodeGoWindows.swift`), portata 1:1. `[SEMPLIFICATO]` niente edge-triggered read-failure reporter persistente — solo logging per-refresh (le query SQLite sono già cheap, a differenza dei parse JSONL) | | OpenRouter | **Fatto** (auth store, usage client, mapper, provider) | `[FEDELE]` solo API key da env var o file di config, nessuna credenziale di sistema | | Z.ai | **Fatto** (auth store, usage client, mapper, provider) | `[FEDELE]` come OpenRouter | | Antigravity | **Fatto** (auth store, metric, usage client, mapper, provider, language server discovery) | `[DIVERGENTE]` `LanguageServerDiscovery`: l'originale usa `ps` + `lsof` (macOS); qui WMI (`Win32_Process.CommandLine` via `System.Management`) per l'elenco processi + `netstat -ano` per le porte in ascolto — vedi `Services/LanguageServerDiscovery.cs`. Keychain → Credential Manager (stessa struttura go-keyring-base64, letta identicamente) | | Kiro | `[DISABILITATO — 0.4.1]` codice e test presenti ma **non registrato in `ProviderCatalog.Make()`** — nessuna installazione lo esegue | `[NUOVO, ora sospeso]` Provider non portato da OpenUsage/Swift (Kiro è un IDE AWS, non esisteva nel progetto originale). Reverse-engineered da `Kiro-Go`/`kiro-gateway` (proxy open source di terze parti) invece che dalla documentazione ufficiale (inesistente per l'usage API). Due fonti di credenziali locali: file JSON Kiro IDE (`~/.aws/sso/cache/kiro-auth-token.json`, che riusa la cartella cache SSO ma non è una vera voce SSO) e il DB SQLite di `kiro-cli` (`%LOCALAPPDATA%\Kiro-Cli\data.sqlite3` su Windows, via `Microsoft.Data.Sqlite` come Cursor/Devin). Endpoint `GET .../getUsageLimits` su `codewhisperer.us-east-1.amazonaws.com` o `q.{region}.amazonaws.com` a seconda della region del profilo CodeWhisperer risolto (mai la region SSO/auth, che può non avere un host dati corrispondente — bug verificato e corretto contro l'account reale usato per il test). Traccia anche l'overage pay-as-you-go (`overageConfiguration`/`overageCap`/`overageRate`/`currentOverages`, stessi campi della risposta base). 15 test unitari sul mapper. `[BUG FIX — 0.3.0 → 0.3.1]` **Refresh proattivo causava un logout forzato ripetuto, anche nell'IDE Kiro stesso.** Il provider, come Codex/Claude/Grok, controllava se il token scadeva entro 5 minuti e lo rinnovava lui stesso via HTTP prima di chiamare `getUsageLimits`. Per Kiro questo è sbagliato: il refresh token AWS SSO/OIDC è a rotazione single-use, e l'IDE Kiro (o `kiro-cli`) rinnova la STESSA sessione in background sul proprio schedule. Il refresh proattivo di AIUsage.NET si è scontrato con quello dell'IDE — confermato dal log reale (`kiro ok` alle 08:17, poi `Session expired` ogni 5 minuti da 08:27 in avanti, sessione mai più recuperata) — e AWS ha reagito revocando l'intera famiglia di token, buttando fuori l'utente anche dall'IDE. Fix: **eliminato ogni refresh proattivo**; il refresh avviene solo in modo reattivo dopo un vero 401/403 dall'API di usage, e prima di chiamare l'endpoint di refresh il provider ri-legge il file/DB dal disco (`ResolveFreshAccessTokenAsync`) — se Kiro ha già rinnovato lui, usa quel token fresco senza toccare la rete. `KiroAuthStore.NeedsRefresh` rimosso (codice morto, nessun altro punto lo chiamava). Riprodotto due volte di seguito contro l'account reale dell'utente prima del fix. `[BUG FIX — 0.3.1, secondo giro]` **Il fix precedente non bastava: il refresh reattivo continuava a ritentare lo stesso refresh token già rifiutato da AWS, ciclo dopo ciclo.** Dopo il primo fix, un token di refresh già rigettato una volta veniva comunque ritentato ad ogni ciclo di refresh successivo (ogni 5 minuti) perché nulla ricordava che era già morto — confermato dal log: da `08:27` a `09:29` (quasi un'ora, ~7 tentativi) il provider ha continuato a chiamare l'endpoint di refresh AWS con lo stesso `refreshToken` scaduto. Lanciando manualmente l'exe per un test (avviando quindi un ottavo tentativo) l'IDE Kiro ha mostrato "Authentication failed. Please sign in again." nello stesso momento — bombardare ripetutamente l'endpoint OIDC con un token già invalidato sembra fare scattare una risposta di sicurezza lato AWS che termina la sessione attiva anche altrove. Fix: `KiroProvider` mantiene ora un dizionario in-memory (mai persistito, mai il token in chiaro — solo `SHA256` dell'ultimo `refreshToken` rifiutato per fonte di credenziali) e, prima di tentare un refresh reattivo, controlla se il token corrente ha lo stesso fingerprint di uno già rifiutato: se sì, fallisce subito con `SessionExpired` senza toccare la rete. Il fingerprint viene scartato automaticamente non appena il token su disco cambia (login effettuato altrove), quindi un nuovo login viene sempre ritentato normalmente. Verificato con due test di regressione (`KiroProviderTests.cs`): uno confronta il conteggio di chiamate all'endpoint di refresh tra due `RefreshAsync()` consecutivi con lo stesso token morto (1, non 2 — fallisce deliberatamente senza il fix, verificato con `git stash`), l'altro conferma che un token ruotato sul disco viene ritentato normalmente. `[BUG FIX — 0.3.0 → 0.3.1]` **`currentOverages` mostrato come se fosse già in dollari — non lo è.** Il mapper leggeva `currentOverages` e lo passava direttamente come importo USD in `MetricLine.Progress(..., ProgressFormat.Dollars)`. Confermato sbagliato contro un account reale: `currentOverages=740.1`, `overageRate=0.04`, spesa reale fatturata $29.60 — cioè `740.1 × 0.04`, non `$740.10`. `currentOverages` è un conteggio di unità (probabilmente richieste) nella stessa unità della riga base, non un importo già prezzato. Fix: la riga Overage moltiplica sempre `currentOverages * overageRate` prima di mostrare un valore in dollari; se manca il rate, mostra un badge "Enabled" invece di inventare un numero. Verificato contro i valori reali riportati dall'utente (740.1/0.04 → $29.60). `[DISABILITATO — 0.4.1]` **Terzo incidente, dopo entrambi i fix sopra: provider disattivato del tutto per tutti.** Nonostante il fix del refresh proattivo (0.3.1) e quello del retry del token morto (0.4.0), l'utente ha segnalato un nuovo logout forzato da Kiro dopo un test della build 0.4.0 — proprio subito dopo il lancio manuale dell'exe pubblicato. La causa esatta di questo terzo episodio non è stata identificata (possibili ipotesi: un terzo percorso di codice che tocca ancora il refresh, come la risoluzione del profile ARN; un'interazione fra le due fonti di credenziali — file desktop e DB SQLite di `kiro-cli` — quando entrambe sono presenti; oppure una finestra di reuse-detection lato AWS più aggressiva di quanto assunto). Decisione: **non tentare un quarto fix incrementale contro l'account reale dell'utente**. Rimossa la riga `providers.Add(new KiroProvider())` da `ProviderCatalog.Make()` (`src/AIUsage.Core/Providers/ProviderCatalog.cs`) — nessuna registrazione, quindi il provider non compare in `WidgetRegistry`, non viene mai eseguito dal refresh loop, e resta invisibile in UI anche per chi l'aveva già abilitato in `enabledProviders.v1` (il suo id semplicemente non fa più match con nulla). Codice e test (`Providers/Kiro/*.cs`, `KiroProviderTests.cs`, `KiroUsageMapperTests.cs`) lasciati intatti e compilano/passano — riattivarlo è una riga sola una volta risolto. Tracciato in [issue #8](https://github.com/LuigiElleBalotta/AIUsage.NET/issues/8). | Pi | `[OMESSO]` | provider "aggregatore" cross-provider (attribuisce uso avvenuto dentro l'app "pi" ad altri provider); rimandato, bassa priorità | **Tutti i 10 provider principali sono ora portati e la solution compila senza errori** (`dotnet build` su `AIUsage.Core`, `AIUsage.Tray`, `AIUsage.Cli`). Nessun test automatico ancora eseguito (nessun progetto di test creato) — la verifica finora è solo "compila", non "si comporta correttamente a runtime". Da fare prima di considerare i provider production-ready: test unitari sui mapper (che sono pure function, facilmente testabili) e un test manuale end-to-end con credenziali reali di almeno un paio di provider (Claude/Codex via CLI, Cursor via editor). ## Infrastruttura di scansione log (JSONL) `[SEMPLIFICATO]` L'originale (`IncrementalJSONLScanner`) è un `actor` con cache **persistita su disco** (property list binari in Application Support, con manifest/versioning, scrittura debounced, pruning delle identità stale, ecc. — vedi `JSONLScanCacheStore.swift`, `JSONLScanCacheCoordination.swift`). Il port C# (`JsonlScanning.cs`, `IncrementalJsonlScanner`) mantiene **solo la cache in-memory** (path+size+mtime), niente persistenza su disco. Impatto: ad ogni riavvio dell'app, il primo refresh ri-scansiona tutti i file di log invece di riutilizzare una cache da un run precedente. Per provider con storici di 30 giorni questo è un costo di CPU/IO occasionale, non un problema di correttezza. Se in futuro serve ottimizzare i tempi di avvio, si può aggiungere persistenza (es. con `System.Text.Json` su file, invece di plist). ## Stores (`Stores/`) — **fatto** `[SEMPLIFICATO]` `WidgetRegistry`, `ProviderEnablementStore`, `DefaultLayout`, `LayoutStore`, `RefreshSetting`, `ProviderSnapshotCache`, `WidgetDataStore` sono tutti portati e la solution compila. Note sulle semplificazioni: - `LayoutStore`: omette lo undo stack, la navigazione a schermate nel popover (Dashboard/Settings) e le transient notice pills della UI SwiftUI — non hanno ancora un equivalente WPF da collegare. Il modello dati (placed widgets, ordine provider/metrica, pin, metriche espanse) è invece portato 1:1. - `WidgetDataStore`: omette l'aggregazione peer-history iCloud, le notifiche di "quota pace" e gli hook di telemetria (nessuno di questi è ancora stato portato lato Windows). Il nucleo — cache con TTL, backoff sui provider falliti, refresh concorrente, risoluzione `MetricLine` → `WidgetData` — è fedele. - `ProviderSnapshotCache`: omette lo stamping dell'identità account (`producedByIdentityKey`), parte della feature multi-account Claude non ancora portata. - `ProviderEnablementStore`: aggiunta la proprietà `EnabledIds` (getter pubblico dell'insieme correntemente abilitato, o `null` se lo store non è mai stato "seedato") — serve a `FirstRunSeeder` per distinguere un install fresh da uno già personalizzato dall'utente (assente nello snippet originale mostrato in `AppContainer.swift`, ma presente nell'edizione Swift completa come `enabledIDs`). `[DIVERGENTE — bug fix]` `CodexUsageMapper.ClassifiedWindowLine`: il codice C# iniziale confrontava un `WindowCandidate?` (record, quindi reference type) con `default(WindowCandidate)` per capire se `FirstOrDefault` avesse trovato qualcosa — ma `FirstOrDefault` su un reference type ritorna `null`, non un'istanza "vuota", quindi `exact.Equals(...)` lanciava `NullReferenceException` ogni volta che nessuna finestra `session`/`weekly` esatta veniva trovata (praticamente sempre, dato che Codex non etichetta le finestre col nome — le classifica per durata). Risultato: **Codex falliva sempre con "Object reference not set to an instance of an object."** anche con credenziali valide. Corretto usando il pattern idiomatico C# (`exact ?? candidates.FirstOrDefault(...)` con controllo `is null`). Trovato testando la CLI end-to-end con credenziali reali — nessun test unitario esisteva per questo percorso. Verificato che dopo il fix `aiusage codex --force` produce dati corretti (sessione, spark, credits) con le credenziali Codex reali di questa macchina. ## App layer (`App/`) — **fatto** (versione minima) `[SEMPLIFICATO]` `AppContainer` (`AIUsage.Core/App/AppContainer.cs`): composition root diretto, porta la costruzione del catalogo provider, `WidgetRegistry`, `ProviderEnablementStore`, `LayoutStore`, `WidgetDataStore`, il first-run seeding e il loop di refresh periodico (5 minuti, `Task.Delay` cancellabile). **Omesso** (nessun equivalente Windows ancora, vedi sotto): multi-account Claude, `ICloudUsageSyncStore`, `NotificationSettingsStore`/quota-pace notifications, `TelemetryRecorder`, `CodexResetClaimService` (il claim dei reset-credit Codex dal popover), `LocalUsageServer` (l'API HTTP locale su :6736), la cattura login-shell environment (non necessaria su Windows, vedi tabella credenziali più sopra). `[FEDELE]` `FirstRunSeeder` (`AIUsage.Core/App/FirstRunSeeder.cs`): stessa logica in due fasi (seed sincrono col fallback Claude/Codex/Cursor, poi probe asincrono delle credenziali locali per sostituire il fallback col set effettivamente rilevato) — porta 1:1 la sequenza Swift, incluso il guard che rispetta un toggle utente cambiato durante il probe. `[OMESSO]` `NewProviderSeeder` (riconciliazione automatica quando un aggiornamento introduce un nuovo provider mai visto da questa installazione) non ancora portato — bassa priorità, serve solo dopo il primo rilascio con un provider aggiunto in un secondo momento. ## CLI (`AIUsage.Cli`) — **fatto** `[FEDELE]` `CliArguments.Parse`: stesso parsing (un provider posizionale opzionale, `--force`, `-v/--version`, `-h/--help`; qualsiasi altra opzione o un secondo provider è un errore d'uso). Stessi codici di uscita: `0` successo, `2` errore d'uso/provider sconosciuto, `4` refresh con warning o errore generico. `[DIVERGENTE]` `UsageReader` (`AIUsage.Core/Services/UsageReader.cs`): condivide lo stesso `ProviderSnapshotCache` su disco della tray app (stesso file `%LOCALAPPDATA%\AIUsage\settings.json`, quindi CLI e tray non divergono mai su "è ancora fresco?"), ma **non** instrada l'output attraverso `LocalUsageAPI` (non ancora portata) — restituisce invece un dump JSON diretto delle `ProviderSnapshot` (displayName/plan/lines/refreshedAt/warning/error), leggibile ma non lo stesso identico schema della risposta HTTP locale Swift. Da rivedere quando/se si porta `LocalUsageAPI`, per tenere un solo schema di risposta condiviso da CLI e API locale (com'è nell'originale). Verificato manualmente con credenziali reali su questa macchina: `aiusage claude --force` e `aiusage codex --force` restituiscono dati corretti (limiti di sessione/settimanali, cronologia spesa, credits Codex). I provider senza credenziali configurate falliscono con messaggi di errore leggibili (non crash), esattamente come nell'originale. ## UI WPF (`AIUsage.Tray`) — rifinita in una sessione successiva, ancora non allo stesso livello del DashboardView SwiftUI `[DIVERGENTE]` Non esiste un'API WPF nativa per un'icona di tray (l'equivalente di `NSStatusItem`). Usato `System.Windows.Forms.NotifyIcon` (richiede `true` oltre a `UseWPF`) — è l'approccio standard per le tray-icon in app WPF. Effetto collaterale: `Application`, `Brush`, `Brushes`, `Orientation`, `HorizontalAlignment`, `CheckBox` sono ambigui tra `System.Windows.*` e `System.Windows.Forms`/`System.Drawing` quando entrambi i toolkit sono referenziati nello stesso progetto; risolto centralizzando gli alias in `AIUsage.Tray/GlobalUsings.cs` (`global using Application = System.Windows.Application;` ecc., WPF vince ovunque) invece di ripetere alias locali in ogni file — `TrayIconFactory`/`TrayController` tornano a usare i tipi WinForms espliciti dove servono davvero (`NotifyIcon`, `ContextMenuStrip`, `MouseButtons`, `Icon`). `[SEMPLIFICATO — ma molto più vicino all'originale ora]` Riscritta la UI con un tema scuro custom (`AIUsage.Tray/Theme/Styles.xaml`): palette coerente con l'originale (sfondo scuro, card più chiare, accent blu, severità normale/warning/critical), bottoni flat con hover/pressed state, scrollbar sottile in stile macOS/VS Code (niente più freccette/thumb di sistema), toggle switch in stile iOS/macOS per i settings. Finestra principale (`MetricsWindow`) ridisegnata come bordo arrotondato (`CornerRadius=14`) con ombra (`DropShadowEffect`), trasparente/`AllowsTransparency` invece del semplice `WindowStyle=None` con sfondo pieno di prima, con una vera title bar draggabile (`DragMove()` su `MouseLeftButtonDown`) e bottoni icona per refresh/settings/close invece del solo bottone testuale "Close". `[FEDELE — nuovo]` Le card per-provider ora hanno: badge dell'icona brand (vedi sotto), nome del provider, pillola col nome del piano (se noto) — la stessa disposizione header vista nello screenshot dell'originale (icona + titolo + badge piano a destra). Ogni riga con un limite (`IsBounded`) ha una vera barra di progresso (non solo testo) colorata per severità (blu/giallo/rosso, leggendo `WidgetData.GetMeterState().Severity` — la stessa logica di pace/severità già portata fedelmente), larga in proporzione a `Fraction`/`RemainingFraction` secondo il `DisplayMode` corrente. `[DIVERGENTE — icone]` Non esiste ancora un renderer SVG runtime nel progetto (una dipendenza come SharpVectors è stata valutata e scartata per ora, per non introdurre una dipendenza NuGet aggiuntiva per un set fisso e conosciuto di 10 icone). `Theme/ProviderIconCatalog.cs` porta invece la geometria `` di ciascun SVG in `Resources/ProviderIcons/*.svg` direttamente come stringhe di `Geometry.Parse` per un `System.Windows.Shapes.Path` WPF (il mini-linguaggio dei path SVG è un sottoinsieme di quello WPF, quindi le stringhe si copiano invariate), dentro un badge colorato con il colore brand di ciascun provider. Se in futuro serve caricare SVG arbitrari (es. icone caricate dall'utente), riconsiderare una libreria come SharpVectors.Wpf. `[NUOVO]` `SettingsWindow` — prima finestra Settings funzionante del port (l'originale non aveva nemmeno un placeholder utile: era un semplice `MessageBox`). Mostra l'elenco dei 10 provider con badge icona, nome, e un toggle collegato direttamente a `AppContainer.Enablement.SetEnabled/IsEnabled` (`ProviderEnablementStore`, già portato fedelmente). `[FEDELE — Customize per-metrica]` Aggiunta una seconda sezione "CUSTOMIZE METRICS" nella stessa `SettingsWindow`: un toggle per ogni `WidgetDescriptor` di ogni provider (non solo l'intero provider), raggruppati sotto un header col nome del provider, collegato direttamente a `LayoutStore.SetMetricEnabled` (già portato fedelmente — la membership in `Placed` era già la fonte di verità, semplicemente non ancora esposta da nessuna UI). Disattivare una singola metrica la rimuove dalla dashboard senza toccare l'abilitazione dell'intero provider. `[OMESSO — resta]` Nessun drag-reorder, nessun pin, nessuna vista "Customize" a schermo intero separata — tutto vive in una sezione scrollabile della stessa finestra Settings, più semplice della Customize screen originale ma con la stessa funzionalità di base (mostra/nascondi singole metriche). `[BUG FIX — race condition]` `WidgetDataStore.RefreshAllAsync` lancia un task `RefreshAsync` per ogni provider in parallelo (`Task.WhenAll`), ma `Snapshots`, `ProviderErrors`, e `_failureRetryAfter` erano `Dictionary<>` semplici scritti senza sincronizzazione da task concorrenti — con più di un provider che falliva nello stesso batch (comune: quasi tutti falliscono per mancanza di credenziali su una macchina pulita), .NET lanciava `InvalidOperationException: A concurrent update was performed on this collection and corrupted its state`, mandando in crash silenzioso l'iterazione del refresh loop (l'eccezione veniva loggata da `AppContainer` ma il batch successivo comunque tentava di ripartire, quindi il sintomo era "la UI non si aggiorna mai più / il log si riempie dell'errore ogni 5 minuti"). Corretto avvolgendo ogni mutazione di quei tre dizionari in un `lock (_mutationLock)`. Trovato durante il test manuale della nuova UI (mai emerso prima perché la UI precedente non veniva rinfrescata abbastanza spesso da esporre la race in una singola sessione di test breve). `[FEDELE — nuovo]` **Icona `.ico` reale**: generata con un piccolo tool standalone (`tools/IconGen/`, non incluso in `AIUsage.sln`, vedi il suo `README.md`) che disegna il glifo brand (badge circolare con gradiente blu + wordmark "AI") alle risoluzioni standard Windows (16, 20, 24, 32, 40, 48, 64, 128, 256px) e le impacchetta in un singolo `.ico` multi-risoluzione con frame compressi PNG. Salvata in `src/AIUsage.Tray/Resources/aiusage.ico`; `AIUsage.Tray.csproj` e `AIUsage.Cli.csproj` la referenziano entrambi via `ApplicationIcon` (quindi sia `AIUsage.exe` che `aiusage.exe` mostrano l'icona vera in Explorer/taskbar/Alt-Tab), e `TrayIconFactory.Create()` continua a caricarla a runtime per l'icona nella tray (il fallback al placeholder disegnato a mano resta solo per il caso in cui il file manchi). Design deliberatamente semplice (non un vero logo definitivo) — da rivalutare se in futuro viene fornito un asset grafico professionale. `[FEDELE — nuovo, grafici]` `MetricsWindow.BuildChart` rende ora le righe `MetricLine.Chart` (Usage Trend): un bar-chart leggero, una barra sottile per giorno, altezza proporzionale al massimo della serie, con un tooltip nativo per barra che mostra `MetricChartPoint.Readout` (lo stesso testo pre-formattato usato altrove, es. "38.1M tokens") e la nota fonte (`ChartNote`) sotto al grafico. `[SEMPLIFICATO]` Non è un'area chart con assi/gridline come l'originale SwiftUI — è un'approssimazione a barre, scelta deliberatamente per evitare di scrivere un motore di plotting completo per un solo tipo di riga; il dato sottostante (30 giorni di storico) è identico. `[FEDELE — nuovo, model breakdown]` Le righe di spesa periodiche (Today/Yesterday/Last 30 Days) con `WidgetData.HasModelBreakdown == true` ora mostrano un tooltip nativo WPF al passaggio del mouse sulla riga, con la lista modelli→token/costo di `ModelUsageBreakdown` (già portato fedelmente in `SpendTileMapper`, semplicemente non ancora consumato da nessuna UI) più un totale e la source note. `[SEMPLIFICATO]` È un `ToolTip` di testo semplice, non il popover `ModelUsageDetail` ricco dell'originale (righe con icone, ordinamento) — stessi dati, presentazione più essenziale. `[OMESSO — resta]` Nessun drag-reorder, nessun pin dalla UI (il modello dati esiste già in `LayoutStore` — `IsPinned`/`SetPinned`/`CanPin` — ma nessuna superficie WPF lo espone ancora), nessuna vera animazione di apertura/altezza, nessuna vibrancy/transparency opzionale. La UI è ormai molto più vicina all'originale (barre di progresso, tema scuro, icone brand, Settings funzionante con toggle provider *e* metrica, grafici Usage Trend, hover breakdown modelli) ma resta una versione più semplice del `DashboardView` SwiftUI completo su questi ultimi dettagli di interazione. Verificato manualmente: l'app si avvia, l'icona compare nella tray, il click sinistro apre/chiude la finestra metriche con il nuovo tema, "Refresh Now" e "Quit" funzionano, la finestra Settings si apre e i toggle provider persistono nello store reale, e il log (`%LOCALAPPDATA%\AIUsage\Logs\AIUsage.log`) mostra più batch di refresh consecutivi completarsi senza più l'eccezione di concorrenza, per tutti e 10 i provider (2 riusciti con credenziali reali — Claude e Codex — 8 falliti con messaggi di errore leggibili per mancanza di credenziali — comportamento atteso su questa macchina di test). ## Local HTTP API (`Services/LocalUsageApi.cs`, `LocalLimitsApi.cs`, `LocalUsageServer.cs`) — **fatto** `[FEDELE]` Porta 1:1 la logica pura di routing/encoding dell'originale (`LocalUsageAPI.swift`, `LocalLimitsAPI.swift`): `LocalUsageApi.Respond(method, path, state)` implementa `/v1/limits`, `/v1/limits/:id`, `/v1/usage`, `/v1/usage/:id`, `OPTIONS` (204 + CORS), 404/405/503 con lo stesso corpo `{"error":"..."}`, e le stesse wire shape documentate in `docs/local-http-api.md` (`progress`/ `text`/`badge`/`barChart` per `/v1/usage`, l'envelope `openusage.limits.v1` con `kind`/`unit`/`used`/ `limit`/`remaining`/`utilization`/`resetsAt`/`windowSeconds`/`estimated` per `/v1/limits`). `LocalLimitsApi.Encode` seleziona solo le risorse esplicitamente dichiarate via `ExportingLimit` sui `WidgetDescriptors` di ogni provider (già tutti portati fedelmente — Claude, Codex, Cursor, ecc. avevano già `.ExportingLimit(...)` scritto, semplicemente non ancora consumato da nessun edge). `[DIVERGENTE]` Il vero listener di rete (`LocalUsageServer.cs`) usa `System.Net.Sockets.TcpListener` su loopback (`127.0.0.1:6736`) invece di `Network.framework` — stesso comportamento (si disabilita silenziosamente in log se la porta è occupata, max 16 connessioni concorrenti con `503` altrimenti, parsing tollerante della prima riga della richiesta). **Bug fix trovato testando dal vivo**: la prima versione recuperava lo `NetworkStream` due volte (`client.GetStream()` sia nella lettura dell'head sia nell'invio della risposta), col primo avvolto in un `using` che chiudeva il socket sottostante prima ancora di scrivere la risposta — ogni richiesta reale falliva con "connessione chiusa in modo imprevisto" nonostante il routing/encoding fossero corretti. Corretto recuperando lo stream una sola volta per l'intera connessione e passandolo sia alla lettura che alla scrittura. `[FEDELE]` `Services/UsageReader.cs` (usato dalla CLI) ora instrada la risposta attraverso `LocalUsageApi.Respond("GET", "/v1/limits[/:id]", state)` esattamente come fa l'originale `UsageReader.swift` — CLI e API locale producono ora lo **stesso identico JSON** per lo stesso provider, invece del dump ad-hoc precedente. Verificato manualmente con credenziali reali: `aiusage claude` produce l'envelope `openusage.limits.v1` corretto; l'app tray con `--show` avvia il server (log `[localapi] listening on 127.0.0.1:6736`) e risponde correttamente a `/v1/limits`, `/v1/usage`, una route ignota (404), e `OPTIONS` (204 + CORS) via richieste HTTP reali da PowerShell. `[OMESSO — resta]` `/v1/limits/:id` e `/v1/usage/:id` fanno matching solo per ID esatto di provider (nessun concetto di "family ID" che nomina più account della stessa famiglia) — corretto per ora dato che il multi-account Claude non è ancora portato; da estendere se/quando lo sarà. Copertura test: `tests/AIUsage.Core.Tests/Services/LocalUsageApiTests.cs` (routing per tutte le route/metodi, ogni tipo di `MetricLine` serializzato correttamente), `tests/AIUsage.Core.Tests/Services/LocalLimitsApiTests.cs` (risorse progress/value, stale flag, provider/risorsa mancante omessi, errori in coda). `[OMESSO]` **iCloud Sync**: nessun equivalente Windows deciso ancora (OneDrive? File locale + nessun sync multi-macchina per ora?). Da discutere. ~~`[OMESSO]` **Sparkle auto-update**~~ — **fatto**: `Services/UpdateChecker.cs` sostituisce Sparkle con un semplice poll delle GitHub Releases (`GET /repos/LuigiElleBalotta/AIUsage.NET/releases/latest`), confronta il `tag_name` con `AppVersion.Display()` (confronto numerico per componenti, non lessicografico — "0.2" > "0.1.9"), e non installa nulla in automatico: apre solo la pagina della release nel browser dell'utente quando sceglie di aggiornare. Throttle di 24h persistito via `ISettingsStore` (`aiusage.updateChecker.lastCheckedAt`) per non interrogare l'API ad ogni avvio, e un "Skip this version" (`aiusage.updateChecker.skippedVersion`) per non riproporre una release già ignorata (una versione più recente verrebbe comunque segnalata). `TrayController` lancia un check silenzioso e throttled a ogni avvio (`CheckForUpdatesOnLaunchAsync`) e aggiunge una voce "Check for Updates..." nel menu della tray icon per un check manuale immediato; se trovato un aggiornamento, in testa al menu compare una voce in grassetto "Update available: vX.Y.Z" (click → apre la release page) più un balloon tip nativo. Nessun download/installazione silenziosa: è deliberatamente il meccanismo più semplice descritto in questa stessa nota, coerente con un progetto open source senza certificato di code-signing (vedi anche `script/release.ps1`). Test in `tests/AIUsage.Core.Tests/Services/UpdateCheckerTests.cs` (confronto versioni, esito rete riuscito/fallito/404, throttling, skip-versione) con un `FakeHttpClient` scriptabile aggiunto ai `TestHelpers` condivisi. `[OMESSO]` **KeyboardShortcuts** (libreria Swift per lo shortcut globale) → da sostituire con `RegisterHotKey`/`UnregisterHotKey` (Win32) o una libreria .NET equivalente (es. `NHotkey`). `[OMESSO]` **PostHog telemetry**: da valutare se portare (SDK PostHog ha un client .NET) o disabilitare di default nella build Windows iniziale. ## Script (richiesta esplicita: .bat/.ps1 al posto di .sh) — **fatto** `[DIVERGENTE]` `script/build_and_run.ps1`: niente app bundle da comporre/firmare come su macOS — è un semplice `dotnet build` + `Start-Process` sull'exe della tray (o `dotnet run` per la CLI con `-Mode cli`). Molto più semplice dell'originale perché .NET non ha l'equivalente della cerimonia "bundle .app + entitlements + iCloud provisioning profile + Sparkle embed + codesign". `[DIVERGENTE]` `script/release.ps1`: `dotnet publish` self-contained/single-file per `win-x64`, zippato in `dist/`. Nessuna firma codice (serve un certificato Authenticode che non abbiamo ancora), nessuna notarizzazione (concetto Apple-only), nessun DMG (concetto macOS-only). Da estendere con `signtool` quando si arriva al packaging per la distribuzione pubblica. `[FEDELE]` `script/update_pricing_snapshots.ps1`: stessa codifica compatta (per-milione, cache write di default = input rate, cache read di default = un decimo, flag `cre:false` per i valori sintetizzati) dello script Python embedded nell'originale, riscritta in PowerShell puro (nessuna dipendenza da Python introdotta in un repo Windows-only). Non ancora eseguito/verificato contro le API live — da testare la prima volta che serve un refresh dei pricing snapshot. Gli script macOS-only dell'originale (`compile_icon.sh`, `embed_sparkle.sh`, `find_icloud_provisioning_profile.sh`, `render_icloud_entitlements.sh`, `apply_github_protections.sh`) non hanno controparte: sono legati a concetti Apple-specifici (Icon Composer, Sparkle framework embedding, iCloud provisioning profiles) che non esistono nel port Windows. `[NUOVO]` `Directory.Build.props` alla radice della solution centralizza il `` per tutti i progetti (letto da `AIUsage.Tray/AppVersion.cs` e `AIUsage.Cli/CliVersion.cs` per mostrare la versione nella title bar della finestra, nella finestra Settings, e in `aiusage --version`). `script/release.ps1` passa `-p:Version=$Version` a `dotnet publish` così l'eseguibile pubblicato riporta sempre la stessa versione del tag git usato per la release, anche se `Directory.Build.props` non fosse stato aggiornato prima del tag. `[BUG FIX — collisione case-insensitive NTFS]` La prima versione di `release.ps1` copiava `AIUsage.exe` (tray) e `aiusage.exe` (CLI) nella stessa cartella per produrre uno zip "flat". NTFS su Windows è case-insensitive per default, quindi `AIUsage.exe` e `aiusage.exe` sono lo **stesso path** per il filesystem: il secondo `Copy-Item` sovrascriveva silenziosamente il primo, e lo zip risultante conteneva solo la CLI (o solo la tray app, a seconda dell'ordine), mai entrambi. Nessun errore visibile — bisognava aprire lo zip per notare che ne mancava uno. Corretto tenendo la CLI nella sua sottocartella `cli/` invece di appiattire tutto in una singola directory. ## Auto-update (`Services/UpdateChecker.cs`) — **fatto** (0.3.1 → 0.4.0, sostituisce l'implementazione GitHub-poll-only) `[DIVERGENTE]` L'originale Swift usa Sparkle (appcast XML firmato + step di installazione in-app). Nessun equivalente .NET diretto degno di essere costruito da zero per un progetto open source a singolo maintainer; scelto invece [Velopack](https://velopack.io) (fork mantenuto di Squirrel.Windows), che legge le GitHub Releases di questo stesso repo tramite `Velopack.Sources.GithubSource` — nessun feed/server separato da hostare. **Cambio di modello di distribuzione, non solo una funzione aggiunta**: l'app passa da zip portabile ("scarica, estrai, lancia l'exe dove vuoi") a installer vero (`Setup.exe`, installazione in `%LocalAppData%\AIUsage.NET\`, voce in "Programmi e funzionalità", shortcut Start Menu). Chi aveva già installato una versione zip precedente all'introduzione di Velopack deve fare un'ultima installazione manuale della prima versione Velopack; da lì in avanti gli aggiornamenti sono davvero automatici (download + apply + restart), non solo un link che apre il browser come nella prima implementazione dell'`UpdateChecker`. `PublishSingleFile` è stato disattivato per la tray app in `release.ps1` — Velopack ha bisogno dei singoli file/DLL per calcolare le patch delta, non di un eseguibile self-extracting; la CLI (`aiusage.exe`, non gestita da Velopack) continua a usare `PublishSingleFile` e viene zippata separatamente. - **`App.xaml.cs`**: `VelopackApp.Build().Run()` chiamato come prima riga di `OnStartup`, prima di `AppLog.Bootstrap` — deve girare prima di qualsiasi altro codice perché al primo lancio dopo un aggiornamento questo re-esegue gli hook post-install/apply di Velopack con argomenti CLI speciali, poi termina il processo prima di raggiungere il resto di `OnStartup`. - **`Services/UpdateChecker.cs`**: riscritto per appoggiarsi a `Velopack.UpdateManager` invece del poll diretto dell'API REST di GitHub. Introdotta `IAppUpdateManager` (interfaccia sottile, stesso pattern di `IHttpClient`/`ISqliteAccessing` già nel progetto) perché la classe concreta `UpdateManager` di Velopack non è pensata per essere mockata nei test unitari; `VelopackAppUpdateManager` è l'implementazione reale, `FakeAppUpdateManager` (nei test) quella finta. `IsNewer` (confronto manuale di stringhe di versione dotted) è stato rimosso — Velopack confronta le versioni internamente via `SemanticVersion`. - **`UpdateChecker.DownloadAndApplyAsync`**: nuovo metodo che scarica l'update e chiama `ApplyUpdatesAndRestart` — l'app termina e riparte nella nuova versione. Prima l'unica azione possibile era aprire la pagina della release nel browser; l'utente doveva scaricare e sostituire gli eseguibili a mano. - **`TrayController.cs`**: la voce di menu "Update available" ora scarica e installa direttamente (con conferma dell'utente via click, mai in automatico in background) invece di apire solo il link della release. - **`script/release.ps1`** e **`.github/workflows/release.yml`**: riscritti per usare `vpk pack` (che produce `Setup.exe`, uno zip portabile, e il feed `releases.win.json` sotto `dist/Releases/`) e `vpk upload github --publish --merge` per allegare tutto alla stessa GitHub Release del tag, invece di zippare l'output di `dotnet publish` a mano. `vpk` è installato come global tool (`dotnet tool install -g vpk`), non tramite `dnx` — `dnx` richiede il .NET 10 SDK, mentre questo progetto builda su .NET 8. - Versione di Velopack pinnata a `1.2.0` (ultima stabile su NuGet al momento; sopra ci sono solo build pre-release `1.2.11x-*`), sia nel package NuGet (`AIUsage.Core.csproj`, `AIUsage.Tray.csproj`) sia nel `vpk` CLI installato in CI, per evitare il disallineamento fra tool e libreria che la stessa documentazione Velopack segnala come causa comune di problemi. `[OMESSO]` Firma del codice (certificato Authenticode via `vpk pack --signParams`) non ancora collegata — stesso limite di prima, SmartScreen avviserà comunque al primo avvio dell'installer. ## Documentazione (`docs/`) — **fatto** `[FEDELE]` Copiata/adattata integralmente: `docs/README.md`, `architecture.md`, `cli.md`, `dashboard.md`, `debugging.md`, `logging.md`, `pricing.md`, `privacy.md`, `provider-enablement.md`, `proxy.md`, `refreshing.md`, `adding-a-provider.md`, più `docs/providers/*.md` per tutti i 10 provider. Riferimenti macOS (`~/Library/...`, Keychain, `.sh`) sostituiti con gli equivalenti Windows (`%LOCALAPPDATA%\AIUsage\Logs`, Credential Manager, `.ps1`) e allineati al comportamento realmente implementato (nessun contenuto aspirazionale). `dashboard.md` aggiornato per riflettere il redesign UI (barre di progresso reali, icone brand, Settings funzionante), continuando a segnalare correttamente come non ancora implementati la Customize per-metrica e i grafici. ## Test automatici (`tests/AIUsage.Core.Tests/`) — **fatto** (copertura mapper + support) `[FEDELE]` Creato il progetto xUnit `tests/AIUsage.Core.Tests` (target `net8.0-windows`, referenzia `AIUsage.Core`), aggiunto a `AIUsage.sln`. **231 test, tutti verdi** (`dotnet test`). Copertura: - **Support**: `Pace` (Evaluate Ahead/OnTrack/Behind/null, `SecondsToRunOut`, `MinimumElapsed`), `MetricFormatter` (Number per Percent/Dollars/Count su stili Tray/Row/Full, `StringFor`, `CostPerMtok`), `LogRedaction` (JWT, API key, devin token, `account=`, path Windows/Unix, URL, body), `ProviderParse`/`StringExtensions` (JsonObject, Number, ClampPercent, CentsToDollars, UnwrapGoKeyring, UrlFormEncoded, NilIfEmpty, TrimmingTrailingSlashes, TitleCased), `AIUsageISO8601` (parse/round-trip), `Formatters` (CompactDuration, WhenLabel, DeadlineLabel, ResetRelativeLabel, Currency, MonthDayLabel). - **Provider mapper**: Claude, Codex, Grok (+ `GrokCreditsConfigDecoder`), Devin, Copilot, Cursor (+ `CursorPlanUsageFacts`), OpenRouter, Z.ai, OpenCode (`OpenCodeGoWindowMath` — session/weekly/ anchored-month math), Antigravity (quota summary parsing, label pooling/normalizzazione, formattazione piano). Ogni mapper ha test per il percorso felice, i casi limite (dati mancanti/ malformati → eccezione con `Kind` corretto), e gli status HTTP di errore (401/500) dove applicabile. - `tests/AIUsage.Core.Tests/TestHelpers/HttpResponseFixture.cs` — helper per costruire `HttpResponseResult` senza chiamate HTTP reali. `[BUG FIX — trovato scrivendo i test]` `PaceTests.SecondsToRunOut_ReturnsPositiveEta_WhenBehind` inizialmente usava uno scenario con `used > limit` (usage già oltre il limite): in quel caso `SecondsToRunOut` ritorna correttamente `null` (non ha senso calcolare un ETA di "esaurimento" se il limite è già superato — `limit - used` è negativo). Corretto il test per usare uno scenario realistico di "Behind per proiezione" (usage ancora sotto il limite ma burn-rate che proietta il superamento prima del reset), aggiunto un test separato per il caso "già oltre il limite → null". Nessun bug nel codice di produzione — solo nel test. Copertura non ancora fatta (bassa priorità, valutare in futuro): test a livello Stores (es. `WidgetDataStore` refresh/cache/backoff — dove è stato trovato il bug di concorrenza reale — e `ProviderEnablementStore` enable/seed logic). ## Stato attuale: porting end-to-end funzionante Con questa sessione il port è **funzionalmente completo e end-to-end**: tutti i 10 provider, lo stores layer, il composition root (`AppContainer`), la CLI (`aiusage`) e una tray app WPF minima ma funzionante sono scritti, compilano senza errori, e sono stati verificati manualmente con credenziali reali su questa macchina (Claude e Codex restituiscono dati corretti; gli altri provider — senza credenziali configurate qui — falliscono con messaggi di errore leggibili, non crash). ## Prossimi passi (rifinitura, non più "completare il porting") Decisione esplicita dell'utente ("dobbiamo portare tutto per avere una versione stabile e completa e funzionante", scelta di scope delegata all'agente): si portano test automatici, log rotation, icona reale, Local HTTP API, Customize per-metrica, grafici, model breakdown hover. Non si portano iCloud Sync, Sparkle auto-update, PostHog telemetry (nessun valore reale per un utente Windows, concetti specifici dell'ecosistema Apple). 1. ~~Test unitari sui mapper di ogni provider~~ — **fatto**, vedi sezione sopra (235 test verdi). 2. ~~Log rotation in `AppLog.cs`~~ — **fatto**, vedi `Support/LogFile.cs` sopra. 3. ~~Icona `.ico` multi-risoluzione reale per tray/exe~~ — **fatto**, vedi sezione UI WPF sopra. 4. ~~`LocalUsageServer`/`LocalUsageAPI` (API HTTP locale su `127.0.0.1:6736`)~~ — **fatto**, vedi sezione "Local HTTP API" sopra; `UsageReader` della CLI condivide ora lo stesso encoder. 5. ~~Customize UI per-metrica~~ — **fatto**, sezione "CUSTOMIZE METRICS" in `SettingsWindow`. 6. ~~Grafici per `MetricLine.Chart` (Usage Trend)~~ — **fatto**, `MetricsWindow.BuildChart`. 7. ~~Popover di breakdown modelli al hover per le righe di spesa~~ — **fatto** (tooltip nativo, vedi sezione UI WPF sopra). 8. Script `.ps1`/`.bat` per build/run — **fatto** (`script/build_and_run.ps1`, `release.ps1`, `update_pricing_snapshots.ps1`). 9. ~~Scorciatoia da tastiera globale, drag-reorder e pin dalla UI, multi-account Claude~~ — **fatto** in una sessione successiva (istruzione esplicita dell'utente: "finire tutto"). Vedi le rispettive sezioni sopra per il dettaglio (`NewProviderSeeder`, drag-reorder/pin, `GlobalHotkeyService`, Claude Desktop fallback, backup locale storico, `UpdateChecker`, multi-account Claude). 10. Quota-pace notifications, PostHog telemetry — restano `[OMESSO]` per scelta deliberata (nessun valore reale per un utente Windows di un progetto OSS single-maintainer). Provider "Pi" (aggregatore cross-provider) — bassa priorità, vedi tabella provider sopra. ## Riepilogo scope "versione stabile e completa" (aggiornato) Oltre ai 6 elementi della sessione precedente (test automatici, log rotation, icona reale, Local HTTP API, Customize per-metrica, grafici Usage Trend + model breakdown hover), una sessione successiva ha portato **tutte** le feature `[OMESSO]` rimanenti (eccetto le tre esplicitamente escluse) su richiesta diretta dell'utente ("devi finire tutto"): `NewProviderSeeder`, drag-reorder e pin (★) dalla UI, scorciatoia da tastiera globale (Ctrl+Alt+U), backup locale export/import dello storico usage (sostituto di iCloud Sync — nessun vero sync multi-macchina, solo un file JSON portabile a scelta dell'utente), fallback alle credenziali di Claude Desktop, `UpdateChecker` (sostituto di Sparkle via GitHub Releases), e multi-account Claude (`ProviderAccountAssembly`, card extra via `CLAUDE_CONFIG_DIR`, rename). Restano deliberatamente fuori scope: quota-pace notifications, PostHog telemetry, provider "Pi" — nessuno di questi ha un impatto reale su un utente Windows di questo fork, e nessuno è stato richiesto esplicitamente. La solution compila senza warning e l'intera suite di test passa (361 test). ## Stato finale di questa sessione ("finire tutto") Istruzione esplicita e diretta dell'utente: portare tutte le feature `[OMESSO]` rimanenti. Fatto in questa sessione (5 commit separati, uno per feature, più il rename/UpdateChecker): `NewProviderSeeder`, drag-reorder e pin (★) dalla UI, scorciatoia da tastiera globale (Ctrl+Alt+U), backup locale export/import dello storico usage (sostituto di iCloud Sync), fallback alle credenziali di Claude Desktop, `UpdateChecker` (sostituto di Sparkle via GitHub Releases), e multi-account Claude completo (`ProviderAccountAssembly`, discovery `CLAUDE_CONFIG_DIR`, rename UI). **Deliberatamente ancora `[OMESSO]`, con motivazione esplicita:** - **PostHog telemetry**: nessun valore reale per un utente Windows di un fork OSS single-maintainer; decisione dell'agente mai contestata dall'utente. - **Quota-pace notifications**: notifiche macOS-native (Notification Center) senza un design WPF equivalente ancora deciso; feature minore rispetto al resto, da riprendere se richiesta. - **Provider "Pi"** (`PiUsageScanner`, aggregatore cross-provider che attribuisce l'uso avvenuto dentro il tool "pi" ad altri provider/card): a differenza delle altre feature, questo non è un bug-fix o un componente isolato — richiederebbe modificare OGNI log scanner esistente (Claude, Codex, OpenCode, ecc.) per fondere le entry pi-originate nelle rispettive Usage Trend/spend tile, per un tool di terze parti che la stragrande maggioranza degli utenti non ha installato. Bassa priorità già nelle note originali pre-sessione; non affrontato per rischio di regressione sui 10 provider già stabili rispetto al beneficio (nullo per chi non usa "pi"). **Verifica finale**: `dotnet build AIUsage.sln` → 0 avvisi, 0 errori. `dotnet test` → 361/361 test verdi. Verificato manualmente contro un'installazione Claude Code reale su questa macchina (singolo account): `aiusage claude --force` restituisce dati corretti passando per l'intera nuova pipeline `ProviderAccountAssembly` → `ProviderCatalog.Make()`, confermando nessuna regressione sul caso a singolo account. Tutti i commit di questa sessione sono stati pushati su `origin/main`. --- *Aggiornare questo file ad ogni sessione di lavoro significativa, aggiungendo nuove voci o spostando elementi da "Omesso/Da fare" a "Fatto" con relative note.*