From fec807a475c37b635289e57441b5c0d59d7f2934 Mon Sep 17 00:00:00 2001 From: Cody Date: Sun, 10 May 2026 23:51:27 -0700 Subject: [PATCH 1/2] Build AppLens desktop dashboard UI --- AppLensDesktop.sln | 15 + docs/AppLens-Typography.md | 23 + src/AppLens.Backend/ReportWriter.cs | 4 +- src/AppLens.Desktop/App.xaml | 11 + src/AppLens.Desktop/AppLens.Desktop.csproj | 2 + src/AppLens.Desktop/DashboardPresentation.cs | 213 ++++++++ src/AppLens.Desktop/MainWindow.xaml | 471 +++++++++++++++--- src/AppLens.Desktop/MainWindow.xaml.cs | 85 +++- .../ReportWriterTests.cs | 10 + .../AppLens.Desktop.Tests.csproj | 30 ++ .../DashboardPresentationTests.cs | 160 ++++++ 11 files changed, 950 insertions(+), 74 deletions(-) create mode 100644 docs/AppLens-Typography.md create mode 100644 src/AppLens.Desktop/DashboardPresentation.cs create mode 100644 tests/AppLens.Desktop.Tests/AppLens.Desktop.Tests.csproj create mode 100644 tests/AppLens.Desktop.Tests/DashboardPresentationTests.cs diff --git a/AppLensDesktop.sln b/AppLensDesktop.sln index d0ea2a8..d505f48 100644 --- a/AppLensDesktop.sln +++ b/AppLensDesktop.sln @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppLens.Backend.Tests", "tests\AppLens.Backend.Tests\AppLens.Backend.Tests.csproj", "{C96B8673-428A-4253-A395-EF23F046FF9A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppLens.Desktop.Tests", "tests\AppLens.Desktop.Tests\AppLens.Desktop.Tests.csproj", "{F81323DD-5671-48F7-9009-91658150D1C0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,6 +61,18 @@ Global {C96B8673-428A-4253-A395-EF23F046FF9A}.Release|x64.Build.0 = Release|Any CPU {C96B8673-428A-4253-A395-EF23F046FF9A}.Release|x86.ActiveCfg = Release|Any CPU {C96B8673-428A-4253-A395-EF23F046FF9A}.Release|x86.Build.0 = Release|Any CPU + {F81323DD-5671-48F7-9009-91658150D1C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F81323DD-5671-48F7-9009-91658150D1C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F81323DD-5671-48F7-9009-91658150D1C0}.Debug|x64.ActiveCfg = Debug|Any CPU + {F81323DD-5671-48F7-9009-91658150D1C0}.Debug|x64.Build.0 = Debug|Any CPU + {F81323DD-5671-48F7-9009-91658150D1C0}.Debug|x86.ActiveCfg = Debug|Any CPU + {F81323DD-5671-48F7-9009-91658150D1C0}.Debug|x86.Build.0 = Debug|Any CPU + {F81323DD-5671-48F7-9009-91658150D1C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F81323DD-5671-48F7-9009-91658150D1C0}.Release|Any CPU.Build.0 = Release|Any CPU + {F81323DD-5671-48F7-9009-91658150D1C0}.Release|x64.ActiveCfg = Release|Any CPU + {F81323DD-5671-48F7-9009-91658150D1C0}.Release|x64.Build.0 = Release|Any CPU + {F81323DD-5671-48F7-9009-91658150D1C0}.Release|x86.ActiveCfg = Release|Any CPU + {F81323DD-5671-48F7-9009-91658150D1C0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -67,5 +81,6 @@ Global {9EB9CA36-8096-4BA1-A96F-8A8A060DA89E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {A63A7527-399E-4482-A344-C9E361B1CF8B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {C96B8673-428A-4253-A395-EF23F046FF9A} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {F81323DD-5671-48F7-9009-91658150D1C0} = {0AB3BF05-4346-4AA6-1389-037BE0695223} EndGlobalSection EndGlobal diff --git a/docs/AppLens-Typography.md b/docs/AppLens-Typography.md new file mode 100644 index 0000000..28e8ea4 --- /dev/null +++ b/docs/AppLens-Typography.md @@ -0,0 +1,23 @@ +# AppLens Typography + +AppLens uses a two-font system across desktop UI, exported reports, and design artifacts. + +## Standard + +- UI text: `Inter`, with `Segoe UI`, `system-ui`, and `sans-serif` fallbacks. +- Technical text: `JetBrains Mono`, with `Cascadia Mono`, `Consolas`, and `monospace` fallbacks. + +Use Inter for navigation, labels, cards, tables, buttons, report prose, and dashboard summaries. Use JetBrains Mono for paths, ledger IDs, correlation IDs, hashes, code-like values, and other local evidence strings where character distinction matters. + +## Implementation Notes + +WinUI references the font families by name and falls back to native Windows fonts when Inter or JetBrains Mono are not installed. Package font assets later if AppLens needs exact typography on every machine. + +HTML reports and design prototypes should keep the shared CSS tokens: + +```css +:root { + --font-ui: "Inter", "Segoe UI", system-ui, sans-serif; + --font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace; +} +``` diff --git a/src/AppLens.Backend/ReportWriter.cs b/src/AppLens.Backend/ReportWriter.cs index 860e98b..9d8e48e 100644 --- a/src/AppLens.Backend/ReportWriter.cs +++ b/src/AppLens.Backend/ReportWriter.cs @@ -75,8 +75,8 @@ public string WriteHtml(AuditSnapshot snapshot, bool includeRawDetails = false) AppLens-desktop Audit Report + + diff --git a/src/AppLens.Desktop/AppLens.Desktop.csproj b/src/AppLens.Desktop/AppLens.Desktop.csproj index 4cafd5b..d892ba1 100644 --- a/src/AppLens.Desktop/AppLens.Desktop.csproj +++ b/src/AppLens.Desktop/AppLens.Desktop.csproj @@ -12,6 +12,8 @@ win-x64 true true + None + true true enable enable diff --git a/src/AppLens.Desktop/DashboardPresentation.cs b/src/AppLens.Desktop/DashboardPresentation.cs new file mode 100644 index 0000000..6dfe1d9 --- /dev/null +++ b/src/AppLens.Desktop/DashboardPresentation.cs @@ -0,0 +1,213 @@ +using System.Globalization; +using System.Text; +using AppLens.Backend; + +namespace AppLens.Desktop; + +public sealed class DashboardPresentation +{ + public DashboardSummaryPresentation Summary { get; init; } = new(); + public List ModuleCards { get; init; } = []; + public List PendingActions { get; init; } = []; + public List RecentLedgerEvents { get; init; } = []; + public string ModuleEmptyState { get; init; } = "No module cards available."; + public string PendingApprovalEmptyState { get; init; } = "No pending Tune approvals."; + public string LedgerEmptyState { get; init; } = "No recent ledger events."; + public string ActiveAppsEmptyState { get; init; } = "Run a scan to populate active app rows."; + + public static DashboardPresentation FromState(AppLensDashboardState state) => + new() + { + Summary = new DashboardSummaryPresentation + { + OverallState = state.Summary.OverallState, + ModuleCoverage = $"{state.Summary.AvailableModuleCount} available / {state.Summary.BlockedModuleCount} blocked", + PendingApprovals = $"{state.Summary.PendingActionCount} pending", + RecentEvents = CountLabel(state.Summary.RecentEventCount, "event"), + LastEvent = state.Summary.LastLedgerEventAt is { } lastEvent + ? FormatTimestamp(lastEvent) + : "No ledger events" + }, + ModuleCards = state.ModuleCards.Select(ToModuleCard).ToList(), + PendingActions = state.PendingActions.Select(ToPendingAction).ToList(), + RecentLedgerEvents = state.RecentLedgerEvents.Select(ToLedgerEvent).ToList() + }; + + public static List BuildActiveAppRows(AuditSnapshot snapshot, int limit = 5) => + snapshot.Tune.TopProcesses + .OrderByDescending(process => process.WorkingSetBytes) + .ThenBy(process => process.Name, StringComparer.OrdinalIgnoreCase) + .Take(limit) + .Select(process => new ActiveAppRowPresentation + { + Name = process.Name, + ProcessId = $"PID {process.Id}", + Memory = Formatting.Size(process.WorkingSetBytes), + Cpu = $"{process.CpuSeconds.ToString("N1", CultureInfo.InvariantCulture)}s", + Relevance = RelevanceFor(process.Name) + }) + .ToList(); + + public static string FormatReadinessScore(AuditSnapshot snapshot) => + snapshot.Readiness.Score.ToString(CultureInfo.InvariantCulture); + + public static string FormatReadinessRating(AuditSnapshot snapshot) => + string.IsNullOrWhiteSpace(snapshot.Readiness.Rating) ? "Review" : snapshot.Readiness.Rating; + + private static ModuleCardPresentation ToModuleCard(ModuleCardReadModel card) => + new() + { + DisplayName = card.DisplayName, + ModuleKind = card.ModuleKind, + Availability = card.StatusLabel, + Risk = string.IsNullOrWhiteSpace(card.RiskLevel) ? "unknown risk" : $"{card.RiskLevel} risk", + Reason = card.Reason, + NextAction = card.NextAction, + CapabilityText = CountLabel(card.CapabilityCount, "capability", "capabilities"), + ActionText = CountLabel(card.ActionCount, "action"), + HealthCheckText = CountLabel(card.HealthCheckCount, "health check"), + StorageRootText = CountLabel(card.StorageRootCount, "storage root"), + RunnableActionText = card.HasRunnableActions ? "Runnable" : "Read-only" + }; + + private static PendingTuneApprovalPresentation ToPendingAction(PendingTuneActionReadModel action) => + new() + { + ProposalId = action.ProposalId, + Kind = Humanize(action.Kind.ToString()), + Target = action.Target, + TargetContext = action.TargetContext, + Risk = string.IsNullOrWhiteSpace(action.RiskLevel) ? "Unknown risk" : $"{action.RiskLevel} risk", + AdminState = action.RequiresAdmin ? "Admin approval" : "User approval", + ProposedAt = FormatTimestamp(action.ProposedAt), + Summary = action.Summary, + CorrelationId = action.CorrelationId + }; + + private static LedgerEventPresentation ToLedgerEvent(LedgerEventReadModel evt) => + new() + { + Type = Humanize(evt.EventType.ToString()), + Module = string.IsNullOrWhiteSpace(evt.ModuleId) ? evt.AppId : evt.ModuleId, + CreatedAt = FormatTimestamp(evt.CreatedAt), + DataState = Humanize(evt.DataState.ToString()).ToLowerInvariant(), + PrivacyState = Humanize(evt.PrivacyState.ToString()).ToLowerInvariant(), + Summary = evt.Summary, + CorrelationId = evt.CorrelationId + }; + + private static string CountLabel(int count, string noun, string? plural = null) => + count == 1 ? $"1 {noun}" : $"{count} {plural ?? $"{noun}s"}"; + + private static string FormatTimestamp(DateTimeOffset timestamp) => + timestamp.ToString("MMM d, yyyy h:mm tt", CultureInfo.InvariantCulture); + + private static string RelevanceFor(string processName) + { + if (processName.Contains("ollama", StringComparison.OrdinalIgnoreCase) + || processName.Contains("llama", StringComparison.OrdinalIgnoreCase)) + { + return "Local AI workload; verify model jobs are intentional."; + } + + if (processName.Contains("docker", StringComparison.OrdinalIgnoreCase)) + { + return "Container runtime; confirm it needs to be active."; + } + + if (processName.Contains("chrome", StringComparison.OrdinalIgnoreCase) + || processName.Contains("msedge", StringComparison.OrdinalIgnoreCase) + || processName.Contains("firefox", StringComparison.OrdinalIgnoreCase)) + { + return "Browser workload; review tab pressure if memory is high."; + } + + if (processName.Equals("code", StringComparison.OrdinalIgnoreCase) + || processName.Contains("devenv", StringComparison.OrdinalIgnoreCase) + || processName.Contains("rider", StringComparison.OrdinalIgnoreCase)) + { + return "Developer tool; check workspace and extension load."; + } + + return "Ranked by current memory pressure."; + } + + private static string Humanize(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return ""; + } + + var builder = new StringBuilder(value.Length + 8); + for (var i = 0; i < value.Length; i++) + { + var current = value[i]; + if (i > 0 && char.IsUpper(current) && !char.IsWhiteSpace(value[i - 1])) + { + builder.Append(' '); + } + + builder.Append(i == 0 ? char.ToUpperInvariant(current) : char.ToLowerInvariant(current)); + } + + return builder.ToString(); + } +} + +public sealed class DashboardSummaryPresentation +{ + public string OverallState { get; init; } = "Ready"; + public string ModuleCoverage { get; init; } = "0 available / 0 blocked"; + public string PendingApprovals { get; init; } = "0 pending"; + public string RecentEvents { get; init; } = "0 events"; + public string LastEvent { get; init; } = "No ledger events"; +} + +public sealed class ModuleCardPresentation +{ + public string DisplayName { get; init; } = ""; + public string ModuleKind { get; init; } = ""; + public string Availability { get; init; } = ""; + public string Risk { get; init; } = ""; + public string Reason { get; init; } = ""; + public string NextAction { get; init; } = ""; + public string CapabilityText { get; init; } = ""; + public string ActionText { get; init; } = ""; + public string HealthCheckText { get; init; } = ""; + public string StorageRootText { get; init; } = ""; + public string RunnableActionText { get; init; } = ""; +} + +public sealed class PendingTuneApprovalPresentation +{ + public string ProposalId { get; init; } = ""; + public string Kind { get; init; } = ""; + public string Target { get; init; } = ""; + public string TargetContext { get; init; } = ""; + public string Risk { get; init; } = ""; + public string AdminState { get; init; } = ""; + public string ProposedAt { get; init; } = ""; + public string Summary { get; init; } = ""; + public string CorrelationId { get; init; } = ""; +} + +public sealed class LedgerEventPresentation +{ + public string Type { get; init; } = ""; + public string Module { get; init; } = ""; + public string CreatedAt { get; init; } = ""; + public string DataState { get; init; } = ""; + public string PrivacyState { get; init; } = ""; + public string Summary { get; init; } = ""; + public string CorrelationId { get; init; } = ""; +} + +public sealed class ActiveAppRowPresentation +{ + public string Name { get; init; } = ""; + public string ProcessId { get; init; } = ""; + public string Memory { get; init; } = ""; + public string Cpu { get; init; } = ""; + public string Relevance { get; init; } = ""; +} diff --git a/src/AppLens.Desktop/MainWindow.xaml b/src/AppLens.Desktop/MainWindow.xaml index d6e61cf..c036539 100644 --- a/src/AppLens.Desktop/MainWindow.xaml +++ b/src/AppLens.Desktop/MainWindow.xaml @@ -4,37 +4,388 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="AppLens-desktop"> - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - -