The EveLens Technical Bible
A forensic comparison: Peter Han's EVEMon v4.0.20 vs EveLens 1.0.0-alpha.28.
A Forensic Comparison: Peter Han's EVEMon v4.0.20 vs EveLens 1.0.0-alpha.28
Every number in this document was extracted from actual source code by reading both codebases file-by-file. No assumptions were made. Every claim is verifiable against the public repositories.
Table of Contents
- The Two Codebases at a Glance
- The God Object: EveMonClient
- The Assembly Split: Monolith to DAG
- The Event System: 69 Static Events to Zero
- The Scheduler: Thundering Herd to Priority Queue
- The ViewModel Layer: 0 to 46
- The UI Framework: WinForms to Avalonia
- The Test Suite: 23 to 1,554
- Cross-Platform: Windows-Only to Three Platforms
- Dead Code Removed: 18,072 Lines
- Settings: XML to JSON
- The Interface Layer: 0 Service Interfaces to 27
- Side-by-Side: File Comparisons
- The Git History: 20 Years in Numbers
- Features: What Actually Works
- Architecture Enforcement: How Regression is Prevented
- The Rebrand: Scope and Precision
- What's Not Done
- The Complete Comparison Table
1. The Two Codebases at a Glance
Peter Han EVEMon v4.0.20 (March 17, 2021)
| Metric | Value |
|---|---|
| .NET Version | Framework 4.6.1 (released 2015) |
| UI Framework | Windows Forms (released 2002) |
| Solution format | Visual Studio 2015 (ToolsVersion 14.0) |
| Projects in solution | 13 (7 source + 3 test + 3 tool) |
| Meaningful source assemblies | 2 (EVEMon + EVEMon.Common) |
| Total C# files | 1,363 |
| Total C# lines | 210,002 |
| EVEMon.Common (the monolith) | 846 files, 83,656 lines |
| EVEMon (WinForms UI) | 251 files, 99,073 lines |
| Tests | 23 in 4 files (417 lines of test code) |
| CI/CD | None |
| ViewModels | 0 |
| Static events | 69 on EveMonClient |
| Service interfaces (DI) | 0 |
| Platforms | Windows only |
| Dark mode / themes | None |
| Last commit | March 17, 2021 |
| Git repository | Not present (ZIP download, no .git) |
EveLens 1.0.0-alpha.28 (March 1, 2026)
| Metric | Value |
|---|---|
| .NET Version | 8.0 (released 2023, LTS) |
| UI Framework | Avalonia 11.2.3 (released 2024) |
| Solution format | SDK-style |
| Projects in solution | 8 (7 source + 1 test) |
| Source assemblies | 7 (Core, Data, Serialization, Models, Infrastructure, Common, Avalonia) |
| Total C# files (src/) | 1,053 |
| Total C# lines (src/) | 123,861 |
| EveLens.Common (reduced) | 507 files, 76,354 lines |
| EveLens.Avalonia (new) | 84 files, 19,115 lines |
| Tests | 1,554 in 110 files (26,605 lines of test code) |
| CI/CD | Promotion system + pre-push hooks (1,790 lines of scripts) |
| ViewModels | 46 production + 10 display wrappers |
| Static events | 0 (EventAggregator: 243 lines) |
| Core interfaces (DI) | 27 in EveLens.Core |
| Platforms | Windows, Linux, macOS |
| Themes | 6 EVE-faction palettes |
| Last commit | March 1, 2026 |
| Total commits (all branches) | 9,807 across 20 years |
2. The God Object: EveMonClient
What It Was
EveMonClient was a public static partial class split across two files totaling 1,856 lines:
EveMonClient.cs— 576 lines (lifecycle, collections, paths, diagnostics)EveMonClient.Events.cs— 1,280 lines (69 static event declarations + 69 OnXxx firing methods)
It served simultaneously as:
- Application lifecycle manager (Initialize, Run, Shutdown)
- Event bus (69 static events, 218 subscriptions across the codebase)
- Service locator (8 static collections, 8 path properties)
- Path manager (EVEMonDataDir, EVEMonCacheDir, etc.)
- Diagnostics system (Trace, traceStream, traceListener)
- Global state holder (Closed, IsDebugBuild, IsSnapshotBuild)
At least 6 distinct responsibilities in a single static type.
The Coupling
| Metric | Count |
|---|---|
| Files referencing EveMonClient | 150 (out of ~1,100 source files) |
| Total EveMonClient references | 929 |
| += event subscriptions | 218 |
| -= event unsubscriptions | 212 |
| Net subscription imbalance (leaks) | 6 |
| References from UI project | 467 |
| References from Common project | 449 |
| Dead events (declared, never subscribed) | 5 |
The 5 dead events:
ESIKeyMonitoredChangedCharacterListUpdatedCharacterPortraitUpdatedCharacterContractBidsDownloadedCorporationContractBidsDownloaded
Events were declared, firing methods were implemented and called, but absolutely nothing subscribed to them. Pure dead code executing on every trigger.
The 6 leaked subscriptions: 5 on TimerTick (EveServer, EveIDToName, EveIDToStation, Settings, G15Handler — all static/singleton objects that intentionally never unsubscribe) plus 1 on CharacterLabelChanged (MainWindow subscribes at line 210, never unsubscribes).
The Top 10 Most-Subscribed Events
| Event | Subscribers |
|---|---|
TimerTick | 35 |
SettingsChanged | 32 |
EveIDToNameUpdated | 14 |
CharacterUpdated | 13 |
PlanChanged | 10 |
ConquerableStationListUpdated | 7 |
PlanNameChanged | 6 |
QueuedSkillsCompleted | 5 |
SchedulerChanged | 4 |
NotificationSent | 4 |
TimerTick fired every 1 second on the UI thread with 35 delegates invoked synchronously via ThreadSafeInvoke. With 30 characters loaded, this meant 35 method invocations per second just for the timer, plus 930+ QueryMonitor polling checks.
What Replaced It
AppServices.cs — 522 lines. A static DI facade with:
- 31 Lazy\
fields (constructors are fast — Law 6) - 45 static service properties (24 interface-backed + 7 collection forwarding + 14 utility)
- 31 internal Set*() methods (for test injection)
- Reset() method (recreates all Lazy fields for test isolation)
- SyncToServiceLocator() (bridges Core assembly to Common services)
Every service that was a static method call on EveMonClient is now behind an interface:
| Old Pattern | New Pattern |
|---|---|
EveMonClient.Characters | AppServices.CharacterRepository.Characters |
EveMonClient.Trace(msg) | AppServices.TraceService?.Trace(msg) |
EveMonClient.OnCharacterUpdated(this) | AppServices.EventAggregator?.Publish(new CharacterUpdatedEvent(...)) |
EveIDToName.GetIDToName(id) | ServiceLocator.NameResolver.GetName(id) |
EveIDToStation.GetIDToStation(id) | ServiceLocator.StationResolver.GetStation(id) |
Settings.UI.MainWindow | Still static (migration target) |
The Strangler Fig pattern is visible: 122 EveLensClient. references remain, being systematically replaced. The old static access still works but new code uses the interface-based pattern.
3. The Assembly Split: Monolith to DAG
Before: 2 Meaningful Assemblies
EVEMon.Common (846 files, 83,656 lines) — EVERYTHING
EVEMon (251 files, 99,073 lines) — WinForms UIEVEMon.Common contained interfaces, enums, DTOs, services, models, serialization, threading, networking, controls, settings, helpers, factories, events — all in one room with no walls.
66 files in EVEMon.Common imported System.Windows.Forms. The business logic was contaminated with the UI framework at the source level.
After: 7 Source Assemblies (Acyclic DAG)
EveLens.Core — 38 files, 2,199 lines — Interfaces only. ZERO dependencies.
↓
EveLens.Data — 173 files, 8,272 lines — Enums, constants, static game data.
↓
EveLens.Serialization — 188 files, 8,187 lines — ESI/EVE/Settings DTOs.
↓
EveLens.Models — 32 files, 2,610 lines — Characters, Skills, Plans.
↓
EveLens.Infrastructure — 31 files, 3,166 lines — EventAggregator, logging, scheduling.
↓
EveLens.Common — 507 files, 76,354 lines — Services, settings, ViewModels.
↓
EveLens.Avalonia — 84 files, 19,115 lines — Cross-platform UI.Dependencies flow strictly downward. AssemblyBoundaryTests.cs (325 lines) uses DFS cycle detection at build time to enforce this. If someone accidentally adds a circular reference, the test fails with a human-readable cycle path.
System.Windows.Forms imports in Common: ZERO (was 66).
4. The Event System: 69 Static Events to Zero
Before: Static Event Soup
// The old way — repeated in every form, 218 times across the codebase
EveMonClient.CharacterUpdated += OnCharacterUpdated;
EveMonClient.SettingsChanged += OnSettingsChanged;
EveMonClient.TimerTick += OnTimerTick;
// ... up to 69 distinct event types
// In OnFormClosing (if the developer remembered):
EveMonClient.CharacterUpdated -= OnCharacterUpdated;
// ... often forgotten → memory leakAll events fired via ThreadSafeInvoke on the UI thread. No type safety beyond EventHandler/EventHandler<T>. No automatic cleanup. No per-character filtering — if you had 30 characters, every subscriber received every event and had to check "is this about my character?"
After: EventAggregator (243 lines)
// The new way — auto-tracked, auto-disposed
_sub = AppServices.EventAggregator.Subscribe<CharacterUpdatedEvent>(OnUpdate);
// _sub.Dispose() in OnDetachedFromVisualTree — tracked by CompositeDisposableImplementation details:
- Thread safety:
ConcurrentDictionary<Type, List<SubscriptionBase>>with per-list locks - Strong subscriptions (default): Hold
Action<TEvent>directly - Weak subscriptions (opt-in):
WeakReference<object>+MethodInfo, allows GC collection - Dead reference cleanup: Lazy during
Publish()—RemoveAll(s => !s.IsAlive) - Exception isolation: Each handler wrapped in try/catch. One failing subscriber never breaks others
- SubscriptionToken:
IDisposableviaInterlocked.Exchangefor thread-safe one-time unsubscribe
Per-character event scoping was added: SubscribeForCharacter<T>() filters at subscription time. 30 characters × 20 event types = 600 subscriptions reduced to 20 (one per event type per character), vs the old pattern where all 600 fired and 580 checked "not me" and returned.
Verified by tests:
EventAggregatorEndToEndTests.cs(409 lines, 14 tests): publish/subscribe, unsubscription, weak refs, concurrent publish (500 tasks), re-entrant publish (10 depth)EventAggregatorScaleTests.cs(317 lines, 9 tests): 100 subscribers, 20 event types, 1000 concurrent threads
5. The Scheduler: Thundering Herd to Priority Queue
Before: 930 Independent Timer-Driven Polls
The original scheduling architecture:
- A
DispatcherTimerfires every 1 second on the UI thread EveMonClient.UpdateOnOneSecondTick()broadcastsTimerTickto 35 subscribers- Each of 31
QueryMonitor<T>instances per character independently checksDateTime.UtcNow > NextUpdate - If due, fires
QueryAsyncCore()→ HTTP request →Dispatcher.Invoke()callback
For 30 characters: 930 QueryMonitor instances × 1 check/second = 930 method invocations per second on the UI thread just for timer checks. Plus CharacterDataQuerying subscribes separately (another 30).
The Thundering Herd: On startup, all monitors have m_forceUpdate = true. First tick fires ~240 HTTP requests simultaneously. ServicePointManager.DefaultConnectionLimit = 10 throttles to 10 concurrent connections. The remaining 230 requests queue. Each response blocks a thread pool thread on Dispatcher.Invoke() waiting for the UI thread. The UI thread is busy processing the next tick's 930 checks. Deadlock potential.
HttpClient per request: new HttpClient(new HttpClientHandler()) for every API call. Classic socket exhaustion anti-pattern.
No proactive rate limiting: The only rate control was EsiErrors.IsErrorCountExceeded — a reactive threshold that stops ALL monitors when error count drops to 10. No per-character awareness.
ETag discarded on pages 2+: Paged queries explicitly passed null for the ETag on subsequent pages, defeating conditional GET optimization.
After: Priority Queue on Background Task (973 lines, 7 files)
UI Thread Background Task (RunAsync)
---------- --------------------------
RegisterCharacter() ─────┐
SetVisibleCharacter() ────┼──> ConcurrentQueue<SchedulerCommand>
ForceRefresh() ───────────┘ │
▼
ProcessCommands() → PriorityQueue<FetchJob, DateTime>
│
▼
ExecuteFetchAsync() → SemaphoreSlim(20)
│
▼
IDispatcher.Post() → UI Thread callbackKey design:
- Command/Query Separation: UI thread pushes commands into
ConcurrentQueue, never touches scheduler state - Priority queue sorted by cache expiry: Zero wasted polling. Scheduler sleeps until next job is due
- SemaphoreSlim(20): 20 concurrent HTTP slots
- Per-character rate limiting:
TokenTrackerwith 150 tokens/15min window, 10% safety margin - Tab-switch awareness:
SetVisibleCharacter()promotes endpoints to Active priority for immediate fetch - Auth failure isolation: 401/403 suspends only that character's jobs
- Cold-start phasing: Phase 1 (0ms) visible char skills → Phase 2 (20ms × index) all characters → Phase 3 (200ms + 10ms × index) alert endpoints → Phase 4 (1s + 50ms × index) everything else
- Lazy invalidation:
ScheduleVersionstamps allow O(1) invalidation without queue scanning - 304 handling: 5 consecutive 304s clears ETag (prevents stale cache nodes). 30-second minimum floor against stale Expires headers
- Session persistence:
PersistState()/RestoreState()withCachedEndpointStatefor warm restart
The old QueryMonitor<T> still exists but is gutted — it no longer initiates HTTP requests. It's now a display-only status adapter. The scheduler's closures call IQueryMonitorEx.SetExternalStatus() to update the UI throbber.
Files:
EsiScheduler.cs— 580 lines (dispatch loop + command processor)FetchPolicy.cs— 93 lines (priority classification, jitter)ColdStartPlanner.cs— 83 lines (phased startup)TokenTracker.cs— 69 lines (per-character rate tracking)TokenBucket.cs— 59 lines (individual rate bucket)FetchJob.cs— 50 lines (scheduled fetch operation)CharacterAuthState.cs— 39 lines (auth health tracking)
Test coverage: 7 files, 630 lines (ColdStartPlannerTests, TokenBucketTests, FetchPolicyTests, TokenTrackerTests, FetchJobTests, CharacterAuthStateTests, FetchQueueTests).
6. The ViewModel Layer: 0 to 46
Before: Copy-Paste Nightmare
EVEMon had 0 ViewModels. All business logic lived inside WinForms controls.
11 list controls (CharacterAssetsList, CharacterMarketOrdersList, CharacterContractsList, etc.) each contained their own filtering, sorting, and grouping logic. The pattern was identical across all 11 but subtly different in each:
// Repeated in every list control (400-1,500 lines each):
private void UpdateContent() // 200+ lines: filter + sort + group + render
private void UpdateContentByGroup<TKey>() // 150 lines: group and render
private bool IsTextMatching() // 30 lines: text search across 10+ fields
private void UpdateSort() // 50 lines: sort logic + visual arrow update421 occurrences of this pattern across 47 files. Fix a sorting bug in assets? The same bug still exists in the other 10 controls.
After: Generic Pipeline + 46 ViewModels
ListViewModel<TItem, TColumn, TGrouping> — 384 lines. Four abstract methods:
GetSourceItems() — where does the data come from?
MatchesFilter() — does this item match the search text?
CompareItems() — how do I compare two items for sorting?
GetGroupKey() — what group does this item belong to?13 concrete list ViewModels, each 72-160 lines, replacing 400-1,500 lines of inline control code:
| ViewModel | Lines | Replaces |
|---|---|---|
| AssetsListViewModel | 108 | ~1,200 lines |
| MarketOrdersListViewModel | 121 | ~800 lines |
| ContractsListViewModel | 160 | ~750 lines |
| IndustryJobsListViewModel | 111 | ~600 lines |
| WalletJournalListViewModel | 158 | ~500 lines |
| WalletTransactionsListViewModel | 107 | ~500 lines |
| MailMessagesListViewModel | 85 | ~400 lines |
| NotificationsListViewModel | 72 | ~400 lines |
| KillLogListViewModel | 153 | ~700 lines |
| PlanetaryListViewModel | 107 | ~600 lines |
| ResearchPointsListViewModel | 96 | ~450 lines |
| ContactsListViewModel | 73 | new |
| StandingsListViewModel | 73 | new |
Total ViewModel layer: 86 files, 13,666 lines (46 production + 10 Avalonia display wrappers + 30 test files).
Every ViewModel is testable in isolation — no WinForms message loop, no static state, no file system access.
7. The UI Framework: WinForms to Avalonia
Before: 99,073 Lines of WinForms
| Metric | Count |
|---|---|
| Total .cs files | 251 |
| Designer.cs files | 116 |
| .resx resource files | 114 |
| Hand-written code | 63,451 lines |
| Designer-generated code | 35,622 lines |
| Forms/windows/dialogs | 52 |
| UserControls | 54 |
| Files over 1,000 lines | 18 |
| Largest file | MainWindow.cs (2,471 lines) |
| Dark mode code | None |
| Theming system | None |
| ViewModel files | 0 |
After: 25,639 Lines of Avalonia (Written from Zero)
| Metric | Count |
|---|---|
| AXAML files | 54 |
| C# files | 80 (47 code-behind + 33 pure) |
| Character monitor views | 21 |
| Dialog windows | 12 |
| Plan editor views | 9 |
| Value converters | 13 |
| Platform service adapters | 7 |
| Theme palettes | 6 |
| Custom controls | 1 (ConstellationCanvas, 834 lines, GPU-accelerated SkiaSharp) |
| Total lines | ~25,639 |
6 EVE-faction dark themes (563 lines of control style overrides + 6 × ~82 lines palette AXAML):
- Dark Space (default) — Navy #0D0D1A / Gold #E6A817
- Caldari Blue — Steel blue #1A2535 / Ice #4FC3F7
- Amarr Gold — Brown #2A2018 / Rich gold #FFB300
- Gallente Green — Deep green #162E22 / Emerald #66BB6A
- Minmatar Rust — Rust brown #261E1A / Copper #FF7043
- Midnight — Pure black #000000 / Bright gold #FFD700
Every view follows the standard wiring pattern (Law 17): OnAttachedToVisualTree → LoadData(), OnDetachedFromVisualTree → dispose subscriptions. No exceptions.
8. The Test Suite: 23 to 1,554
Before: 23 Tests
| File | Tests | What It Tests |
|---|---|---|
AsyncVoidMethodsTests.cs (Common) | 1 | Reflection scan for async void |
AsyncVoidMethodsTests.cs (UI) | 1 | Reflection scan for async void |
CompressionUncompressionTests.cs | 6 | GZip/Deflate round-trip |
TimeExtensionsTests.cs | 15 | CCP date format + remaining time |
Not tested: Characters, skills, plans, settings, serialization, ESI, market orders, contracts, industry, UI behavior, events, networking, data integrity, architecture boundaries. Nothing.
Framework: xUnit 2.1 (from 2015). Moq referenced but never used. 417 lines of test code.
After: 1,554 Tests (verified by test runner — Passed: 1,554, Failed: 0, Skipped: 0)
| Category | Files | Lines | Tests |
|---|---|---|---|
| Architecture enforcement | 3 | 987 | 32 |
| Models (Character, ESI, Plans, etc.) | 13 | 3,497 | ~200 |
| Services (AppServices, EventAggregator, etc.) | 20 | 5,679 | ~280 |
| Settings (round-trip, migration, JSON) | 8 | 3,282 | ~140 |
| Integration (E2E, scale, stress) | 7 | 2,524 | ~100 |
| Serialization (ESI DTOs, settings DTOs) | 4 | 1,490 | ~80 |
| ViewModels (all 46 VMs + pipeline) | 30 | 4,493 | ~310 |
| Regression (crash prevention, GitHub issues) | 3 | 845 | 44 |
| Scheduling (TokenBucket, ColdStart, etc.) | 7 | 667 | ~48 |
| Helpers (file manager, batching) | 3 | 1,692 | ~60 |
| Logging (TCP stream) | 2 | 227 | ~20 |
| QueryMonitor | 2 | 409 | ~30 |
| Net | 1 | 158 | ~15 |
| Root-level + test doubles | 6 | 651 | ~30 |
| TOTAL | 109 | 26,605 | 1,554 |
Framework: xUnit 2.9.2 + FluentAssertions 6.12.2 + NSubstitute 5.1.0. 2,915 FluentAssertions calls, 107 NSubstitute mock creations, 125 async test methods.
Growth factor: 67.6x more tests. Test-to-source ratio went from 1:470 to approximately 1:4.7.
9. Cross-Platform: Windows-Only to Three Platforms
Before: Windows Forever
- Target:
.NET Framework 4.6.1(Windows-only) - 66
using System.Windows.Formsimports in the business logic layer System.Drawing.Imageproperties on 7 model classesSystem.Windows.Forms.SortOrderused in 2 comparers in the model layerDispatcherwrapping WPF'sSystem.Windows.Threading.DispatcherTimerMessageBox.Show()called from 5 files in Common- No path abstraction (hardcoded
%APPDATA%\EVEMon)
After: Windows + Linux + macOS
- Target:
net8.0(portable, no platform-specific TFM) - ZERO
System.Windows.Formsimports in EveLens.Common - 7 platform service adapters in EveLens.Avalonia/Services/
- XDG-compliant path resolution:
- - Windows:
%APPDATA%\EveLens - - Linux:
$XDG_CONFIG_HOME/EveLens(defaults to~/.config/EveLens) - - macOS:
~/Library/Application Support/EveLens - Native notifications:
- - Windows: WinRT toast notifications via reflection (avoids net8.0-windows TFM)
- - Linux:
notify-send(libnotify) - - macOS:
osascript display notification - Single-instance detection: file-based lock (
FileShare.None) + optional namedEventWaitHandle - X11 WmClass set for Linux window manager integration
- PNG icon on Linux/macOS (ICO has poor X11 compatibility)
- 13 consecutive alpha releases (alpha.4 through alpha.25) fixing Linux/macOS settings persistence
- DPAPI disabled (pass-through) — avoids
PlatformNotSupportedException - Synchronous shutdown saves for Linux/X11 (async dispatcher unreliable)
10. Dead Code Removed: 18,072 Lines
Code present in the original fork that is dead — either depending on services that no longer exist, hardware that's discontinued, or game features CCP removed.
| Category | Files | Lines | Dead Since |
|---|---|---|---|
| IGB (In-Game Browser) Service | 6 | 1,091 | March 2017 (CCP removed IGB) |
| Cloud Storage (Dropbox/GDrive/OneDrive) | 14 | 2,612 | ~2018 (deprecated SDKs) |
| Certificate Browser | 33+ | 4,986 | June 2014 (CCP removed certificates) |
| Hammertime API | 2 | 420 | ~2020 (API shut down) |
| Logitech G15 Keyboard LCD | 8+ | 1,668 | ~2013 (hardware discontinued) |
| EVEMon.Sales (Mineral Worksheet) | 13 | 1,525 | ~2016-2018 (EVE-Central/BattleClinic dead) |
| EVEMon.PieChart (3D pie chart) | 13 | 4,299 | N/A (WinForms GDI+ only) |
| EVEMon.Watchdog | 7 | 357 | N/A (hardcodes "EVEMon" process name) |
| Osmium/BattleClinic Loadouts | 3+ | 223+ | 2016-2019 (sites shut down) |
| Market Pricers (EVEMarketer/Fuzzworks) | 8+ | 783+ | ~2020 (EVEMarketer dead) |
| EVEMon.WindowsApi | 5 | 108 | N/A (Windows 7 feature detection) |
| TOTAL | ~112 | ~18,072 |
The IGB server contained code for CCPEVE.requestTrust() — a JavaScript API that only existed inside the now-removed In-Game Browser. The Logitech G15 handler rendered to a 160x43 pixel monochrome LCD on a keyboard discontinued in 2008. The Certificate Browser operated on game data CCP removed in 2014. Every line was executing against something that no longer exists.
11. Settings: XML to JSON
Before
- Format: XML via
System.Xml.Serialization.XmlSerializer - Root element:
<Settings>withrevisionattribute - ESI tokens: Plain-text XML attributes (
[XmlAttribute("refreshToken")]) - SSO credentials: Plain-text XML attributes on root
<Settings>element - Proxy passwords: Plain-text XML
- XSLT transform: 116-line backward-compatibility transform applied before deserialization
- Save mechanism: 1-second timer poll with 10-second debounce,
XmlSerializer.Serialize()to file - MessageBox.Show(): 6 calls in Settings.cs for error handling
- Settings file: Single
settings.xmlin%APPDATA%\EVEMon\ - Bug:
GetRevisionNumber()regex returned 0 for bothrevision="0"and missing attribute (Issue #4)
After
- Format: JSON (source of truth), XML backward-compatible read
- Save mechanism:
SmartSettingsManagerwith coalescing + multi-file JSON viaSettingsFileManager - Per-character storage: Atomic per-component files in character subdirectories
- Settings migration: Automatic from
%APPDATA%\EVEMonto%APPDATA%\EveLens - Fork detection:
forkIdattribute differentiates our users from peterhaneve users - Save diagnostics: Full trace logging on every save cycle
- Cross-platform: Synchronous shutdown save for Linux/X11 deadlock avoidance
- Thread safety:
volatileon critical fields,Task.Run()for file I/O - Total Settings code: 1,642 lines across 4 partial class files (was 724 lines in 1 file)
12. The Interface Layer: 0 Service Interfaces to 27
Before: 21 Interfaces, 0 for Services
The original fork had 21 interfaces, all inside EVEMon.Common. None were service/DI interfaces:
| Interface | Purpose | Problem |
|---|---|---|
IReadonlyCollection<T> | Custom read-only collection | Predates BCL IReadOnlyCollection<T> |
IListView | WinForms ListView abstraction | References System.Windows.Forms |
IQueryMonitor | Query monitor status | Defines own Dispose() instead of IDisposable |
IStaticSkill | Static skill data | Exposes mutable Collection<T> |
IColumnSettings | Column settings | Extends deprecated ICloneable |
No IEventAggregator, no IDispatcher, no ISettingsProvider, no ICharacterRepository, no IEsiClient, no ITraceService, no IApplicationPaths. Zero testability infrastructure.
After: 27 Service Interfaces in EveLens.Core (Zero Dependencies)
| Interface | Members | Purpose |
|---|---|---|
IEventAggregator | 4 | Pub/sub messaging |
IDispatcher | 3 | UI thread marshaling |
ISettingsProvider | 5 | Settings access |
ICharacterRepository | 6+ | Character collection (combined ICharacterReader + ICharacterWriter) |
IEsiClient | 4 | Rate-limited ESI access |
IEsiScheduler | 10 | Priority-based ESI fetching |
ICharacterDataCache | 5 | Persist ESI data to disk |
ITraceService | 6 | Diagnostic logging |
IApplicationPaths | 5 | Cross-platform file paths |
IDialogService | 4 | Platform-agnostic dialogs |
IClipboardService | 2 | Platform-agnostic clipboard |
IApplicationLifecycle | 2 | Exit/Restart |
IScreenInfo | 2 | Display geometry |
INameResolver | 3 | Entity ID-to-name |
IStationResolver | 2 | Station ID resolver |
IFlagResolver | 2 | Inventory flag lookup |
IImageService | 1 | Image download + cache |
INotificationService | 3 | Alert notifications |
INotificationTypeResolver | 4 | Notification type lookup |
IResourceProvider | 2 | Embedded resources |
ICharacterFactory | 4 | Character creation + tracking |
ICharacterIdentity | 4 | Minimal character contract |
ICharacterQueryManager | 4 | Per-character ESI monitors |
ICharacterReader | 6 | Read-only character access |
ICharacterWriter | 1 | Character mutation |
IESITokenProvider | 2 | ESI OAuth token access |
IStation | 3 | Station/structure info |
Every interface lives in EveLens.Core — the leaf assembly with zero dependencies. WinForms adapters served the old UI. Avalonia adapters serve the new one. Same interfaces, different implementations. Swap the adapter, swap the platform.
13. Side-by-Side: File Comparisons
Program.cs
| Aspect | Original (283 lines) | EveLens (143 lines) |
|---|---|---|
| Framework | WinForms Application.Run() | Avalonia BuildAvaloniaApp().StartWithClassicDesktopLifetime() |
| Startup | StartupAsync().Wait() (sync-over-async) | Synchronous: set AUMID, check instance, set culture, launch |
| Initialization | All in Program.cs: file paths, trace, cloud storage, update manager, EveMonClient.Initialize(), Settings.Initialize() | Deferred to App.axaml.cs (12-phase bootstrap) |
| Static mutable state | 3 fields (s_mainWindow, s_exitRequested, s_errorWindowIsShown) | 0 fields (Law 1) |
| Crash handling | Custom UnhandledExceptionWindow + Google Analytics | Removed entirely |
| Cross-platform | Windows-only (Win7 AppID) | Windows AUMID + X11 WmClass |
Settings.cs
| Aspect | Original (724 lines, 1 file) | EveLens (1,642 lines, 4 files) |
|---|---|---|
| Timer | EveMonClient.TimerTick += handler (static event, 1-second) | EventAggregator.Subscribe<ThirtySecondTickEvent>() (30-second) |
| Async void | No try/catch | try/catch wrapping (Law 10) |
| Event notification | EveMonClient.OnSettingsChanged() | EventAggregator.Publish(SettingsChangedEvent.Instance) |
| Format | XML only | JSON (source of truth) + XML backward read |
| Platform | MessageBox.Show() in 6 places | AppServices.DialogService |
Character.cs
| Aspect | Original (1,072 lines) | EveLens (1,216 lines) |
|---|---|---|
| Interface | Character : BaseCharacter | Character : BaseCharacter, ICharacterIdentity |
| Monitored property | EveMonClient.MonitoredCharacters.Contains(this) | ServiceLocator.CharacterRepository.IsMonitored(this) |
| Station resolution | EveIDToStation.GetIDToStation(id) | ServiceLocator.StationResolver.GetStation(id) |
| Event publishing | EveMonClient.OnCharacterUpdated(this) | ServiceLocator.EventAggregator.Publish(new CharacterUpdatedEvent(...)) |
| New in EveLens | N/A | Booster detection system (~130 lines): math-based detection using attribute invariant |
14. The Git History: 20 Years in Numbers
| Metric | Value |
|---|---|
| First commit | August 2, 2006 ("Permissions Test") |
| Total commits (all branches) | 9,807 |
| Total contributors | 57+ named |
| Release tags | 91 |
| Branches (local + remote) | 98 |
Contributors by Commit Count
| Author | Commits | Era |
|---|---|---|
| Jimi C (Dimitris Charalampidis) | 5,886 | 2010-2016 |
| Richard Slater | 1,494 | 2007-2009 |
| BradStone | 526 | 2006-2008 |
| Alia Collins | 356 | 2026 |
| Peter Han | 328 | 2018-2021 |
| Eewec | 267 | 2006-2007 |
| 36+ others | ~600 | Various |
The Key Dates
| Date | Event |
|---|---|
| 2006-08-02 | First commit (Six Anari) |
| 2011 | Peak year (954 commits) |
| 2016-12-03 | Last Dev Team release (v3.0.3) |
| 2018-05-12 | Peter Han's v4.0.0 (ESI migration) |
| 2021-03-17 | Last upstream commit (v4.0.20) |
| 2021-2025 | 4.75 years dormant |
| 2026-01-04 | Resurrection (.NET 8 migration) |
| 2026-02-15 | Project Phoenix (6-assembly split, 821 tests) |
| 2026-02-17 | Avalonia replaces WinForms |
| 2026-02-26 | Rebrand to EveLens |
| 2026-03-01 | Current: 1.0.0-alpha.28 |
15. Features: What Actually Works
Verified by reading every view file's LoadData() method and checking the AXAML for TabItem presence.
Working (32 features)
| Feature | Status |
|---|---|
| Character Overview (card grid with portraits, ISK, SP, training, ESI status dots) | WORKING |
| Skills tab (grouped, 5-block level indicators, filter, collapse/expand) | WORKING |
| Skill Queue tab (progress bars, training/completed/pending) | WORKING |
| Employment History (horizontal timeline, async corp logos) | WORKING |
| Standings (grouped, entity portraits, standing bars) | WORKING |
| Contacts (standing-based color gradient, watchlist stars) | WORKING |
| Medals (title, reason, issuer, corp, date) | WORKING |
| Loyalty Points (corp name, LP count, sorted desc) | WORKING |
| Assets (hierarchical tree + flat grouping, filter, estimated value) | WORKING |
| Market Orders (DataGrid, hide inactive, issued-for filter) | WORKING |
| Contracts (DataGrid, outstanding/completed counts, hide inactive) | WORKING |
| Industry Jobs (DataGrid, hide inactive, issued-for filter) | WORKING |
| Wallet Journal (grouped, type filter, net amount, green/red coloring) | WORKING |
| Wallet Transactions (DataGrid, 9 grouping modes) | WORKING |
| Mail Messages (grouped, opens reading window, async body download) | WORKING |
| Notifications (grouped, 6 grouping modes) | WORKING |
| Kill Log (grouped, kills/losses distinction, async ship images) | WORKING |
| Plan Editor (5 tabs: Plan, Skills, Ships, Items, Blueprints) | WORKING |
| Settings (6 sections, sidebar navigation, theme selector) | WORKING |
| Skill Constellation (GPU-accelerated SkiaSharp, pan/zoom/search) | WORKING |
| 6 Theme Palettes (DarkSpace, Caldari, Amarr, Gallente, Minmatar, Midnight) | WORKING |
| ESI Scope Editor (per-scope granularity, 3 presets) | WORKING |
| SSO Authentication (OAuth2 PKCE, local HTTP callback) | WORKING |
| About Window (Hall of Fame, 57+ contributors, GPL text) | WORKING |
| Native OS Notifications (Windows toast, Linux notify-send, macOS osascript) | WORKING |
| Per-Category Privacy Mode (6 categories, eye toggle, flyout checkboxes) | WORKING |
| Per-Character Data Cache | WORKING |
| ESI Endpoint Toggle (per-character enable/disable with gear flyout) | WORKING |
| Manage Groups (create, rename, delete, assign characters) | WORKING |
| Implant Set Editor (10 slot combos, new/rename/delete, attribute summary) | WORKING |
| Attribute Remap Dialog | WORKING |
| Activity Log / Notification Center (bell icon, unread badge, mark read, clear) | WORKING |
Partial (2)
| Feature | Status |
|---|---|
| Planetary (PI) tab | Code-behind + ViewModel exist, TabItem NOT in AXAML |
| Research Points tab | Code-behind + ViewModel exist, TabItem NOT in AXAML |
Not Working (4)
| Feature | Status |
|---|---|
| Character Comparison | Placeholder "Coming soon" dialog |
| Booster / Cerebral Accelerator Simulation | Zero references in Avalonia project |
| Mass Re-auth | Zero references in Avalonia project |
| Factional Warfare tab | Code-behind exists but never populates data, no TabItem |
16. Architecture Enforcement: How Regression is Prevented
Three files form a three-layer defense against architectural regression:
Layer 1: AssemblyBoundaryTests.cs (325 lines, 11 tests)
Uses runtime reflection to inspect assembly references. Enforces the acyclic DAG:
- Core has zero EveLens dependencies
- Data only depends on Core
- No assembly references one above it
- DFS cycle detection with human-readable cycle paths on failure
Layer 2: ViewModelArchitectureTests.cs (487 lines, 17 tests)
- All VMs inherit ViewModelBase
- No VM references System.Windows.Forms
- All VMs implement IDisposable
- No VM exceeds 150 declared members
- All list VMs have matching Avalonia views
- All old filter methods (
IsTextMatching,UpdateSort,UpdateContentByGroup) deleted from delegated controls GroupedItemsnever null when no character setObservableCharacterstays under 30 properties with no collections
Layer 3: WinFormsCouplingTests.cs (176 lines, 4 tests)
- Image properties on models return
object?, notSystem.Drawing.Image - Service files don't call
MessageBox.Show()directly - SSO services use
AppServices.Dispatcher, not static WinForms dispatcher
These tests run on every build. The architecture can't regress because the tests won't let it.
17. The Rebrand: Scope and Precision
| Metric | Count |
|---|---|
| "EveLens" references in source | 4,820 |
| "EVEMon" references remaining | 1,067 |
| Accidental "EVEMon" leftovers | 0 |
The 1,067 remaining "EVEMon" references break down as:
- ~1,050: Copyright headers (
© 2006-2021 EVEMon Development Team) — legally required under GPL v2 - 4: About window historical attribution
- 3: Migration code (
%APPDATA%\EVEMon→%APPDATA%\EveLens) - 3: External Bitbucket URLs (cannot be changed)
Zero "EVEMon" references exist in class names, namespaces, identifiers, settings keys, or UI labels.
18. What's Not Done
Honest accounting of remaining work:
- 122
EveLensClient.references in Common — mostly collection access, being strangled - 637
Settings.references —Settings.UI.*accounts for ~400 (UI layer config) - Forms use
AppServices.*static access rather than constructor injection - 3 tabs coded but not wired (Planetary, Research Points, Factional Warfare)
- Character Comparison — placeholder
- Booster simulation — was in WinForms era, did not work as intended, not ported
- Mass re-auth — was in WinForms era, never shipped, not ported
- No true DI container — AppServices is a static Strangler Fig facade, not a proper
IServiceProvider
19. The Complete Comparison Table
| Dimension | Peter Han EVEMon v4.0.20 | EveLens 1.0.0-alpha.28 |
|---|---|---|
| .NET | Framework 4.6.1 | 8.0 |
| UI | WinForms | Avalonia 11.2.3 |
| Platforms | Windows | Windows + Linux + macOS |
| Source assemblies | 2 | 7 |
| Tests | 23 | 1,554 |
| Test lines | 417 | 26,605 |
| ViewModels | 0 | 46 + 10 display |
| Static events | 69 | 0 |
| Core interfaces | 0 service | 27 |
| Lazy services | 0 | 31 |
| AXAML views | 0 | 54 |
| Theme palettes | 0 | 6 |
| CI/CD | None | Promotion system (1,790 lines) |
| Architectural laws | 0 | 14 (enforced by tests) |
| Architecture tests | 0 | 32 |
| Dead code carried | 18,072 lines | Removed |
| WinForms imports in Common | 66 | 0 |
| Settings format | XML (plain-text tokens) | JSON (multi-file, per-character) |
| ESI scheduling | 930 independent polls/sec (UI thread) | Priority queue (background thread) |
| Rate limiting | Reactive (error threshold only) | Per-character token buckets |
| Diagnostic stream | None | TCP JSON-lines (port 5555) |
| ESI scope control | All-or-nothing | Per-scope editor with 3 presets |
| Privacy mode | None | 6-category granular |
| Skill visualization | None | GPU-accelerated SkiaSharp constellation |
| Native notifications | None | Windows toast + Linux notify-send + macOS osascript |
| Activity log | None | Persistent, 200 entries, JSON storage |
| God object | 1,856 lines, 929 references | Frozen (Law 9), being strangled |
This document was compiled from 27 parallel analysis agents that read every file in both codebases. Every number is from actual code. Every claim is verifiable.
EveLens is free, open source, and available at evelens.dev. Source: github.com/aliacollins/evelens