All posts

The EveLens Technical Bible

A forensic comparison: Peter Han's EVEMon v4.0.20 vs EveLens 1.0.0-alpha.28.

Engineers·30 min read
01

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.

02

Table of Contents

  1. The Two Codebases at a Glance
  2. The God Object: EveMonClient
  3. The Assembly Split: Monolith to DAG
  4. The Event System: 69 Static Events to Zero
  5. The Scheduler: Thundering Herd to Priority Queue
  6. The ViewModel Layer: 0 to 46
  7. The UI Framework: WinForms to Avalonia
  8. The Test Suite: 23 to 1,554
  9. Cross-Platform: Windows-Only to Three Platforms
  10. Dead Code Removed: 18,072 Lines
  11. Settings: XML to JSON
  12. The Interface Layer: 0 Service Interfaces to 27
  13. Side-by-Side: File Comparisons
  14. The Git History: 20 Years in Numbers
  15. Features: What Actually Works
  16. Architecture Enforcement: How Regression is Prevented
  17. The Rebrand: Scope and Precision
  18. What's Not Done
  19. The Complete Comparison Table
03

1. The Two Codebases at a Glance

Peter Han EVEMon v4.0.20 (March 17, 2021)

MetricValue
.NET VersionFramework 4.6.1 (released 2015)
UI FrameworkWindows Forms (released 2002)
Solution formatVisual Studio 2015 (ToolsVersion 14.0)
Projects in solution13 (7 source + 3 test + 3 tool)
Meaningful source assemblies2 (EVEMon + EVEMon.Common)
Total C# files1,363
Total C# lines210,002
EVEMon.Common (the monolith)846 files, 83,656 lines
EVEMon (WinForms UI)251 files, 99,073 lines
Tests23 in 4 files (417 lines of test code)
CI/CDNone
ViewModels0
Static events69 on EveMonClient
Service interfaces (DI)0
PlatformsWindows only
Dark mode / themesNone
Last commitMarch 17, 2021
Git repositoryNot present (ZIP download, no .git)

EveLens 1.0.0-alpha.28 (March 1, 2026)

MetricValue
.NET Version8.0 (released 2023, LTS)
UI FrameworkAvalonia 11.2.3 (released 2024)
Solution formatSDK-style
Projects in solution8 (7 source + 1 test)
Source assemblies7 (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
Tests1,554 in 110 files (26,605 lines of test code)
CI/CDPromotion system + pre-push hooks (1,790 lines of scripts)
ViewModels46 production + 10 display wrappers
Static events0 (EventAggregator: 243 lines)
Core interfaces (DI)27 in EveLens.Core
PlatformsWindows, Linux, macOS
Themes6 EVE-faction palettes
Last commitMarch 1, 2026
Total commits (all branches)9,807 across 20 years
04

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

MetricCount
Files referencing EveMonClient150 (out of ~1,100 source files)
Total EveMonClient references929
+= event subscriptions218
-= event unsubscriptions212
Net subscription imbalance (leaks)6
References from UI project467
References from Common project449
Dead events (declared, never subscribed)5

The 5 dead events:

  1. ESIKeyMonitoredChanged
  2. CharacterListUpdated
  3. CharacterPortraitUpdated
  4. CharacterContractBidsDownloaded
  5. CorporationContractBidsDownloaded

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

EventSubscribers
TimerTick35
SettingsChanged32
EveIDToNameUpdated14
CharacterUpdated13
PlanChanged10
ConquerableStationListUpdated7
PlanNameChanged6
QueuedSkillsCompleted5
SchedulerChanged4
NotificationSent4

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 PatternNew Pattern
EveMonClient.CharactersAppServices.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.MainWindowStill 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.

05

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 UI

EVEMon.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).

06

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 leak

All 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 CompositeDisposable

Implementation 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: IDisposable via Interlocked.Exchange for 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
07

5. The Scheduler: Thundering Herd to Priority Queue

Before: 930 Independent Timer-Driven Polls

The original scheduling architecture:

  1. A DispatcherTimer fires every 1 second on the UI thread
  2. EveMonClient.UpdateOnOneSecondTick() broadcasts TimerTick to 35 subscribers
  3. Each of 31 QueryMonitor<T> instances per character independently checks DateTime.UtcNow > NextUpdate
  4. 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 callback

Key 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: TokenTracker with 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: ScheduleVersion stamps 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() with CachedEndpointState for 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).

08

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 update

421 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:

ViewModelLinesReplaces
AssetsListViewModel108~1,200 lines
MarketOrdersListViewModel121~800 lines
ContractsListViewModel160~750 lines
IndustryJobsListViewModel111~600 lines
WalletJournalListViewModel158~500 lines
WalletTransactionsListViewModel107~500 lines
MailMessagesListViewModel85~400 lines
NotificationsListViewModel72~400 lines
KillLogListViewModel153~700 lines
PlanetaryListViewModel107~600 lines
ResearchPointsListViewModel96~450 lines
ContactsListViewModel73new
StandingsListViewModel73new

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.

09

7. The UI Framework: WinForms to Avalonia

Before: 99,073 Lines of WinForms

MetricCount
Total .cs files251
Designer.cs files116
.resx resource files114
Hand-written code63,451 lines
Designer-generated code35,622 lines
Forms/windows/dialogs52
UserControls54
Files over 1,000 lines18
Largest fileMainWindow.cs (2,471 lines)
Dark mode codeNone
Theming systemNone
ViewModel files0

After: 25,639 Lines of Avalonia (Written from Zero)

MetricCount
AXAML files54
C# files80 (47 code-behind + 33 pure)
Character monitor views21
Dialog windows12
Plan editor views9
Value converters13
Platform service adapters7
Theme palettes6
Custom controls1 (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): OnAttachedToVisualTreeLoadData(), OnDetachedFromVisualTree → dispose subscriptions. No exceptions.

10

8. The Test Suite: 23 to 1,554

Before: 23 Tests

FileTestsWhat It Tests
AsyncVoidMethodsTests.cs (Common)1Reflection scan for async void
AsyncVoidMethodsTests.cs (UI)1Reflection scan for async void
CompressionUncompressionTests.cs6GZip/Deflate round-trip
TimeExtensionsTests.cs15CCP 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)

CategoryFilesLinesTests
Architecture enforcement398732
Models (Character, ESI, Plans, etc.)133,497~200
Services (AppServices, EventAggregator, etc.)205,679~280
Settings (round-trip, migration, JSON)83,282~140
Integration (E2E, scale, stress)72,524~100
Serialization (ESI DTOs, settings DTOs)41,490~80
ViewModels (all 46 VMs + pipeline)304,493~310
Regression (crash prevention, GitHub issues)384544
Scheduling (TokenBucket, ColdStart, etc.)7667~48
Helpers (file manager, batching)31,692~60
Logging (TCP stream)2227~20
QueryMonitor2409~30
Net1158~15
Root-level + test doubles6651~30
TOTAL10926,6051,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.

11

9. Cross-Platform: Windows-Only to Three Platforms

Before: Windows Forever

  • Target: .NET Framework 4.6.1 (Windows-only)
  • 66 using System.Windows.Forms imports in the business logic layer
  • System.Drawing.Image properties on 7 model classes
  • System.Windows.Forms.SortOrder used in 2 comparers in the model layer
  • Dispatcher wrapping WPF's System.Windows.Threading.DispatcherTimer
  • MessageBox.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.Forms imports 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 named EventWaitHandle
  • 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)
12

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.

CategoryFilesLinesDead Since
IGB (In-Game Browser) Service61,091March 2017 (CCP removed IGB)
Cloud Storage (Dropbox/GDrive/OneDrive)142,612~2018 (deprecated SDKs)
Certificate Browser33+4,986June 2014 (CCP removed certificates)
Hammertime API2420~2020 (API shut down)
Logitech G15 Keyboard LCD8+1,668~2013 (hardware discontinued)
EVEMon.Sales (Mineral Worksheet)131,525~2016-2018 (EVE-Central/BattleClinic dead)
EVEMon.PieChart (3D pie chart)134,299N/A (WinForms GDI+ only)
EVEMon.Watchdog7357N/A (hardcodes "EVEMon" process name)
Osmium/BattleClinic Loadouts3+223+2016-2019 (sites shut down)
Market Pricers (EVEMarketer/Fuzzworks)8+783+~2020 (EVEMarketer dead)
EVEMon.WindowsApi5108N/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.

13

11. Settings: XML to JSON

Before

  • Format: XML via System.Xml.Serialization.XmlSerializer
  • Root element: <Settings> with revision attribute
  • 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.xml in %APPDATA%\EVEMon\
  • Bug: GetRevisionNumber() regex returned 0 for both revision="0" and missing attribute (Issue #4)

After

  • Format: JSON (source of truth), XML backward-compatible read
  • Save mechanism: SmartSettingsManager with coalescing + multi-file JSON via SettingsFileManager
  • Per-character storage: Atomic per-component files in character subdirectories
  • Settings migration: Automatic from %APPDATA%\EVEMon to %APPDATA%\EveLens
  • Fork detection: forkId attribute 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: volatile on 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)
14

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:

InterfacePurposeProblem
IReadonlyCollection<T>Custom read-only collectionPredates BCL IReadOnlyCollection<T>
IListViewWinForms ListView abstractionReferences System.Windows.Forms
IQueryMonitorQuery monitor statusDefines own Dispose() instead of IDisposable
IStaticSkillStatic skill dataExposes mutable Collection<T>
IColumnSettingsColumn settingsExtends 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)

InterfaceMembersPurpose
IEventAggregator4Pub/sub messaging
IDispatcher3UI thread marshaling
ISettingsProvider5Settings access
ICharacterRepository6+Character collection (combined ICharacterReader + ICharacterWriter)
IEsiClient4Rate-limited ESI access
IEsiScheduler10Priority-based ESI fetching
ICharacterDataCache5Persist ESI data to disk
ITraceService6Diagnostic logging
IApplicationPaths5Cross-platform file paths
IDialogService4Platform-agnostic dialogs
IClipboardService2Platform-agnostic clipboard
IApplicationLifecycle2Exit/Restart
IScreenInfo2Display geometry
INameResolver3Entity ID-to-name
IStationResolver2Station ID resolver
IFlagResolver2Inventory flag lookup
IImageService1Image download + cache
INotificationService3Alert notifications
INotificationTypeResolver4Notification type lookup
IResourceProvider2Embedded resources
ICharacterFactory4Character creation + tracking
ICharacterIdentity4Minimal character contract
ICharacterQueryManager4Per-character ESI monitors
ICharacterReader6Read-only character access
ICharacterWriter1Character mutation
IESITokenProvider2ESI OAuth token access
IStation3Station/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.

15

13. Side-by-Side: File Comparisons

Program.cs

AspectOriginal (283 lines)EveLens (143 lines)
FrameworkWinForms Application.Run()Avalonia BuildAvaloniaApp().StartWithClassicDesktopLifetime()
StartupStartupAsync().Wait() (sync-over-async)Synchronous: set AUMID, check instance, set culture, launch
InitializationAll in Program.cs: file paths, trace, cloud storage, update manager, EveMonClient.Initialize(), Settings.Initialize()Deferred to App.axaml.cs (12-phase bootstrap)
Static mutable state3 fields (s_mainWindow, s_exitRequested, s_errorWindowIsShown)0 fields (Law 1)
Crash handlingCustom UnhandledExceptionWindow + Google AnalyticsRemoved entirely
Cross-platformWindows-only (Win7 AppID)Windows AUMID + X11 WmClass

Settings.cs

AspectOriginal (724 lines, 1 file)EveLens (1,642 lines, 4 files)
TimerEveMonClient.TimerTick += handler (static event, 1-second)EventAggregator.Subscribe<ThirtySecondTickEvent>() (30-second)
Async voidNo try/catchtry/catch wrapping (Law 10)
Event notificationEveMonClient.OnSettingsChanged()EventAggregator.Publish(SettingsChangedEvent.Instance)
FormatXML onlyJSON (source of truth) + XML backward read
PlatformMessageBox.Show() in 6 placesAppServices.DialogService

Character.cs

AspectOriginal (1,072 lines)EveLens (1,216 lines)
InterfaceCharacter : BaseCharacterCharacter : BaseCharacter, ICharacterIdentity
Monitored propertyEveMonClient.MonitoredCharacters.Contains(this)ServiceLocator.CharacterRepository.IsMonitored(this)
Station resolutionEveIDToStation.GetIDToStation(id)ServiceLocator.StationResolver.GetStation(id)
Event publishingEveMonClient.OnCharacterUpdated(this)ServiceLocator.EventAggregator.Publish(new CharacterUpdatedEvent(...))
New in EveLensN/ABooster detection system (~130 lines): math-based detection using attribute invariant
16

14. The Git History: 20 Years in Numbers

MetricValue
First commitAugust 2, 2006 ("Permissions Test")
Total commits (all branches)9,807
Total contributors57+ named
Release tags91
Branches (local + remote)98

Contributors by Commit Count

AuthorCommitsEra
Jimi C (Dimitris Charalampidis)5,8862010-2016
Richard Slater1,4942007-2009
BradStone5262006-2008
Alia Collins3562026
Peter Han3282018-2021
Eewec2672006-2007
36+ others~600Various

The Key Dates

DateEvent
2006-08-02First commit (Six Anari)
2011Peak year (954 commits)
2016-12-03Last Dev Team release (v3.0.3)
2018-05-12Peter Han's v4.0.0 (ESI migration)
2021-03-17Last upstream commit (v4.0.20)
2021-20254.75 years dormant
2026-01-04Resurrection (.NET 8 migration)
2026-02-15Project Phoenix (6-assembly split, 821 tests)
2026-02-17Avalonia replaces WinForms
2026-02-26Rebrand to EveLens
2026-03-01Current: 1.0.0-alpha.28
17

15. Features: What Actually Works

Verified by reading every view file's LoadData() method and checking the AXAML for TabItem presence.

Working (32 features)

FeatureStatus
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 CacheWORKING
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 DialogWORKING
Activity Log / Notification Center (bell icon, unread badge, mark read, clear)WORKING

Partial (2)

FeatureStatus
Planetary (PI) tabCode-behind + ViewModel exist, TabItem NOT in AXAML
Research Points tabCode-behind + ViewModel exist, TabItem NOT in AXAML

Not Working (4)

FeatureStatus
Character ComparisonPlaceholder "Coming soon" dialog
Booster / Cerebral Accelerator SimulationZero references in Avalonia project
Mass Re-authZero references in Avalonia project
Factional Warfare tabCode-behind exists but never populates data, no TabItem
18

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
  • GroupedItems never null when no character set
  • ObservableCharacter stays under 30 properties with no collections

Layer 3: WinFormsCouplingTests.cs (176 lines, 4 tests)

  • Image properties on models return object?, not System.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.

19

17. The Rebrand: Scope and Precision

MetricCount
"EveLens" references in source4,820
"EVEMon" references remaining1,067
Accidental "EVEMon" leftovers0

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.

20

18. What's Not Done

Honest accounting of remaining work:

  • 122 EveLensClient. references in Common — mostly collection access, being strangled
  • 637 Settings. referencesSettings.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
21

19. The Complete Comparison Table

DimensionPeter Han EVEMon v4.0.20EveLens 1.0.0-alpha.28
.NETFramework 4.6.18.0
UIWinFormsAvalonia 11.2.3
PlatformsWindowsWindows + Linux + macOS
Source assemblies27
Tests231,554
Test lines41726,605
ViewModels046 + 10 display
Static events690
Core interfaces0 service27
Lazy services031
AXAML views054
Theme palettes06
CI/CDNonePromotion system (1,790 lines)
Architectural laws014 (enforced by tests)
Architecture tests032
Dead code carried18,072 linesRemoved
WinForms imports in Common660
Settings formatXML (plain-text tokens)JSON (multi-file, per-character)
ESI scheduling930 independent polls/sec (UI thread)Priority queue (background thread)
Rate limitingReactive (error threshold only)Per-character token buckets
Diagnostic streamNoneTCP JSON-lines (port 5555)
ESI scope controlAll-or-nothingPer-scope editor with 3 presets
Privacy modeNone6-category granular
Skill visualizationNoneGPU-accelerated SkiaSharp constellation
Native notificationsNoneWindows toast + Linux notify-send + macOS osascript
Activity logNonePersistent, 200 entries, JSON storage
God object1,856 lines, 929 referencesFrozen (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