Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions AppLensDesktop.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
23 changes: 23 additions & 0 deletions docs/AppLens-Typography.md
Original file line number Diff line number Diff line change
@@ -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;
}
```
4 changes: 2 additions & 2 deletions src/AppLens.Backend/ReportWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ public string WriteHtml(AuditSnapshot snapshot, bool includeRawDetails = false)
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AppLens-desktop Audit Report</title>
<style>
:root { color-scheme: light; --ink:#14151a; --muted:#656b76; --line:#d9dde5; --accent:#0b6bcb; --surface:#f5f7fb; }
body { margin:0; font-family:"Segoe UI", Arial, sans-serif; color:var(--ink); background:white; }
:root { color-scheme: light; --ink:#14151a; --muted:#656b76; --line:#d9dde5; --accent:#0b6bcb; --surface:#f5f7fb; --font-ui:"Inter", "Segoe UI", system-ui, sans-serif; --font-mono:"JetBrains Mono", "Cascadia Mono", Consolas, monospace; }
body { margin:0; font-family:var(--font-ui); color:var(--ink); background:white; }
header { padding:32px 40px; color:white; background:#101820; }
main { padding:28px 40px 44px; }
h1 { margin:0 0 8px; font-size:32px; letter-spacing:0; }
Expand Down
11 changes: 11 additions & 0 deletions src/AppLens.Desktop/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>

<FontFamily x:Key="AppLensUiFontFamily">Inter, Segoe UI, Arial</FontFamily>
<FontFamily x:Key="AppLensMonoFontFamily">JetBrains Mono, Cascadia Mono, Consolas</FontFamily>

<Style TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource AppLensUiFontFamily}" />
</Style>

<Style x:Key="AppLensMonoTextBlockStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource AppLensMonoFontFamily}" />
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>
2 changes: 2 additions & 0 deletions src/AppLens.Desktop/AppLens.Desktop.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">win-x64</RuntimeIdentifier>
<UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling>
<WindowsPackageType Condition="'$(GenerateAppxPackageOnBuild)' != 'true'">None</WindowsPackageType>
<WindowsAppSDKSelfContained Condition="'$(GenerateAppxPackageOnBuild)' != 'true'">true</WindowsAppSDKSelfContained>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
272 changes: 272 additions & 0 deletions src/AppLens.Desktop/DashboardPresentation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
using System.Globalization;
using System.Text;
using AppLens.Backend;

namespace AppLens.Desktop;

public sealed class DashboardPresentation
{
public DashboardSummaryPresentation Summary { get; init; } = new();
public DashboardRailPresentation Rail { get; init; } = new();
public List<ModuleCardPresentation> ModuleCards { get; init; } = [];
public List<PendingTuneApprovalPresentation> PendingActions { get; init; } = [];
public List<LedgerEventPresentation> 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"
},
Rail = new DashboardRailPresentation
{
DashboardBadge = state.Summary.OverallState.Equals("Ready", StringComparison.OrdinalIgnoreCase) ? "live" : "action",
InventoryBadge = state.Summary.ModuleCount.ToString(CultureInfo.InvariantCulture),
TunePlanBadge = state.Summary.PendingActionCount.ToString(CultureInfo.InvariantCulture),
ReportsBadge = state.Summary.RecentEventCount.ToString(CultureInfo.InvariantCulture),
Modules = state.ModuleCards.Select(ToModuleRailBadge).ToList()
},
ModuleCards = state.ModuleCards.Select(ToModuleCard).ToList(),
PendingActions = state.PendingActions.Select(ToPendingAction).ToList(),
RecentLedgerEvents = state.RecentLedgerEvents.Select(ToLedgerEvent).ToList()
};

public static List<ActiveAppRowPresentation> 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 ModuleRailBadgePresentation ToModuleRailBadge(ModuleCardReadModel card) =>
new()
{
DisplayName = card.DisplayName,
Badge = card.Availability switch
{
ModuleAvailability.Available => "ok",
ModuleAvailability.Blocked => "blocked",
_ => "check"
}
};

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 DashboardRailPresentation
{
public string DashboardBadge { get; init; } = "live";
public string InventoryBadge { get; init; } = "0";
public string TunePlanBadge { get; init; } = "0";
public string ReportsBadge { get; init; } = "0";
public List<ModuleRailBadgePresentation> Modules { get; init; } = [];
}

public sealed class ModuleRailBadgePresentation
{
public string DisplayName { get; init; } = "";
public string Badge { get; init; } = "";
}

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; } = "";
}

public static class DashboardWindowSizing
{
public static DashboardWindowBounds Calculate(DashboardWorkArea workArea, double scale)
{
var safeScale = double.IsFinite(scale) && scale > 0 ? scale : 1d;
var desiredWidth = (int)Math.Round(1180 * safeScale);
var desiredHeight = (int)Math.Round(820 * safeScale);
var margin = (int)Math.Round(40 * safeScale);
var availableWidth = Math.Max(1, workArea.Width - margin * 2);
var availableHeight = Math.Max(1, workArea.Height - margin * 2);
var width = Math.Min(desiredWidth, availableWidth);
var height = Math.Min(desiredHeight, availableHeight);
var x = workArea.X + Math.Max(0, (workArea.Width - width) / 2);
var y = workArea.Y + Math.Max(0, (workArea.Height - height) / 2);

return new DashboardWindowBounds(x, y, width, height);
}
}

public sealed record DashboardWorkArea(int X, int Y, int Width, int Height);

public sealed record DashboardWindowBounds(int X, int Y, int Width, int Height);
Loading
Loading