Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Set up .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: "9.0.100"
dotnet-version: "10.0.x"

- name: Install bun
uses: oven-sh/setup-bun@v2
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
Expand Down
7 changes: 7 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,12 @@
<ItemGroup>
<PackageVersion Include="Mindscape.Raygun4Net.AspNetCore" Version="11.2.1" />
<PackageVersion Include="Mindscape.Raygun4Net.NetCore.Common" Version="11.2.1" />
<PackageVersion Include="Serilog" Version="4.3.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" />
<PackageVersion Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Sinks.Raygun" Version="8.2.0" />
<PackageVersion Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="8.0.0" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,16 @@ [FromQuery] DateTime endTime

return PartialView("_ErrorTimeseries", errorTimeseries);
}

[HttpGet("/crashreporting/error-group-count")]
public async Task<IActionResult> ErrorGroupCount(
[FromQuery] string applicationIdentifier,
[FromQuery] string errorGroupId,
[FromQuery] DateTime startTime,
[FromQuery] DateTime endTime
)
{
var count = await _raygunApiService.GetErrorGroupCountAsync(applicationIdentifier, errorGroupId, startTime, endTime);
return Content(count.ToString("N0"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@model Minigun.Models.ErrorGroup

@using Minigun.Utilities

<tr class="bg-white border-b hover:bg-gray-50">
<td class="w-12 p-4">
<div class="flex items-center">
<input id="checkbox-@Model.Identifier" type="checkbox" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500">
<label for="checkbox-@Model.Identifier" class="sr-only">checkbox</label>
</div>
</td>
<td class="px-4 py-2 font-medium text-gray-900 truncate">
<a href="@Model.ApplicationUrl" class="font-medium text-blue-600 hover:underline">@Model.Message</a>
</td>
<td class="px-4 py-2 whitespace-nowrap text-gray-600">
@{
var lastSeenText = Model.LastOccurredAt.ToHumanReadableTimeAgo();
}
@lastSeenText
</td>
<td class="px-4 py-2 whitespace-nowrap text-gray-600">
@{
var firstSeenText = Model.CreatedAt.ToHumanReadableTimeAgo();
}
@firstSeenText
</td>
<td class="px-4 py-2 whitespace-nowrap text-gray-700 font-semibold"
hx-get="@Url.Action("ErrorGroupCount", "CrashReporting", new { Area = "CrashReporting" })"
hx-vals='{"applicationIdentifier": "@Context.Request.Query["applicationIdentifier"]", "errorGroupId": "@Model.Identifier", "startTime": "@Context.Request.Query["startTime"]", "endTime": "@Context.Request.Query["endTime"]"}'
hx-trigger="intersect once"
hx-target="#count-@Model.Identifier"
hx-indicator=".no-global-indicator">
<span id="count-@Model.Identifier" class="text-gray-400 animate-pulse">–</span>
</td>
</tr>
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@
<label for="checkbox-all" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-4 py-2 w-2/3">Message</th>
<th scope="col" class="px-4 py-2 w-2/5">Message</th>
<th scope="col" class="px-4 py-2 w-1/6">Last seen</th>
<th scope="col" class="px-4 py-2 w-1/6">Status</th>
<th scope="col" class="px-4 py-2 w-1/6">First seen</th>
<th scope="col" class="px-4 py-2 w-1/6">Count</th>
</tr>
</thead>
</table>
Expand All @@ -41,60 +42,172 @@
</div>

<div class="m-4 bg-white border border-secondary-blue-grey">
<div class="flex flex-wrap text-sm font-medium text-center text-gray-500 border-b border-gray-200">
<div class="px-4 py-3 bg-gray-100">
Active <span class="ml-2 bg-red-500 text-white rounded-full px-2 py-1">@Model.Count(e => e.Status == "active")</span>
</div>
<div class="px-4 py-3">
Resolved <span class="ml-2 bg-green-500 text-white rounded-full px-2 py-1">@Model.Count(e => e.Status == "resolved")</span>
</div>
<div class="px-4 py-3">
Ignored <span class="ml-2 bg-blue-500 text-white rounded-full px-2 py-1">@Model.Count(e => e.Status == "ignored")</span>
</div>
<div class="px-4 py-3">
Permanently ignored <span class="ml-2 bg-yellow-500 text-white rounded-full px-2 py-1">@Model.Count(e => e.Status == "permanentlyignored")</span>
</div>
@{
var activeCount = Model.Count(e => e.Status == "active");
var resolvedCount = Model.Count(e => e.Status == "resolved");
var ignoredCount = Model.Count(e => e.Status == "ignored");
var permanentlyIgnoredCount = Model.Count(e => e.Status == "permanentlyignored");
}

<div class="flex flex-wrap text-sm font-medium text-center border-b border-gray-200">
<button class="px-4 py-3 bg-gray-100 status-tab active font-bold !text-black" data-status="active" @(activeCount == 0 ? "disabled" : "")>
Active <span class="ml-2 bg-red-500 text-white rounded-full px-2 py-1">@activeCount</span>
</button>
<button class="px-4 py-3 status-tab text-gray-500 @(resolvedCount == 0 ? "cursor-not-allowed opacity-50" : "hover:bg-gray-50")" data-status="resolved" @(resolvedCount == 0 ? "disabled" : "")>
Resolved <span class="ml-2 bg-green-500 text-white rounded-full px-2 py-1">@resolvedCount</span>
</button>
<button class="px-4 py-3 status-tab text-gray-500 @(ignoredCount == 0 ? "cursor-not-allowed opacity-50" : "hover:bg-gray-50")" data-status="ignored" @(ignoredCount == 0 ? "disabled" : "")>
Ignored <span class="ml-2 bg-blue-500 text-white rounded-full px-2 py-1">@ignoredCount</span>
</button>
<button class="px-4 py-3 status-tab text-gray-500 @(permanentlyIgnoredCount == 0 ? "cursor-not-allowed opacity-50" : "hover:bg-gray-50")" data-status="permanentlyignored" @(permanentlyIgnoredCount == 0 ? "disabled" : "")>
Permanently ignored <span class="ml-2 bg-yellow-500 text-white rounded-full px-2 py-1">@permanentlyIgnoredCount</span>
</button>
</div>

<table class="w-full text-sm text-left text-gray-500 table-fixed">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-4 w-12">
<div class="flex items-center">
<input id="checkbox-all" type="checkbox" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500">
<label for="checkbox-all" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-4 py-2 w-2/3">Message</th>
<th scope="col" class="px-4 py-2 w-1/6">Last seen</th>
<th scope="col" class="px-4 py-2 w-1/6">Status</th>
</tr>
</thead>
<tbody>
@foreach (var errorGroup in Model)
{
<tr class="bg-white border-b hover:bg-gray-50">
<td class="w-12 p-4">
@{
var activeErrors = Model.Where(e => e.Status == "active");
var resolvedErrors = Model.Where(e => e.Status == "resolved");
var ignoredErrors = Model.Where(e => e.Status == "ignored");
var permanentlyIgnoredErrors = Model.Where(e => e.Status == "permanentlyignored");
}

<!-- Active Errors -->
<div class="status-content" data-status="active">
<table class="w-full text-sm text-left text-gray-500 table-fixed">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-4 w-12">
<div class="flex items-center">
<input id="checkbox-@errorGroup.Identifier" type="checkbox" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500">
<label for="checkbox-@errorGroup.Identifier" class="sr-only">checkbox</label>
<input id="checkbox-all-active" type="checkbox" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500">
<label for="checkbox-all-active" class="sr-only">checkbox</label>
</div>
</td>
<td class="px-4 py-2 font-medium text-gray-900 truncate">
<a href="@errorGroup.ApplicationUrl" class="font-medium text-blue-600 hover:underline">@errorGroup.Message</a>
</td>
<td class="px-4 py-2 whitespace-nowrap text-red-600">
@{
var lastSeenText = errorGroup.LastOccurredAt.ToHumanReadableTimeAgo();
}
@lastSeenText
</td>
<td class="px-4 py-2 whitespace-nowrap">
@(errorGroup.ResolvedIn != null ? "Resolved" : "Active")
</td>
</th>
<th scope="col" class="px-4 py-2 w-2/5">Message</th>
<th scope="col" class="px-4 py-2 w-1/6">Last seen</th>
<th scope="col" class="px-4 py-2 w-1/6">First seen</th>
<th scope="col" class="px-4 py-2 w-1/6">Count</th>
</tr>
}
</tbody>
</table>
</thead>
<tbody>
@foreach (var errorGroup in activeErrors)
{
@await Html.PartialAsync("_ErrorGroupRow", errorGroup)
}
</tbody>
</table>
</div>

<!-- Resolved Errors -->
<div class="status-content" data-status="resolved" style="display: none;">
<table class="w-full text-sm text-left text-gray-500 table-fixed">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-4 w-12">
<div class="flex items-center">
<input id="checkbox-all-resolved" type="checkbox" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500">
<label for="checkbox-all-resolved" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-4 py-2 w-2/5">Message</th>
<th scope="col" class="px-4 py-2 w-1/6">Last seen</th>
<th scope="col" class="px-4 py-2 w-1/6">First seen</th>
<th scope="col" class="px-4 py-2 w-1/6">Count</th>
</tr>
</thead>
<tbody>
@foreach (var errorGroup in resolvedErrors)
{
@await Html.PartialAsync("_ErrorGroupRow", errorGroup)
}
</tbody>
</table>
</div>

<!-- Ignored Errors -->
<div class="status-content" data-status="ignored" style="display: none;">
<table class="w-full text-sm text-left text-gray-500 table-fixed">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-4 w-12">
<div class="flex items-center">
<input id="checkbox-all-ignored" type="checkbox" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500">
<label for="checkbox-all-ignored" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-4 py-2 w-2/5">Message</th>
<th scope="col" class="px-4 py-2 w-1/6">Last seen</th>
<th scope="col" class="px-4 py-2 w-1/6">First seen</th>
<th scope="col" class="px-4 py-2 w-1/6">Count</th>
</tr>
</thead>
<tbody>
@foreach (var errorGroup in ignoredErrors)
{
@await Html.PartialAsync("_ErrorGroupRow", errorGroup)
}
</tbody>
</table>
</div>

<!-- Permanently Ignored Errors -->
<div class="status-content" data-status="permanentlyignored" style="display: none;">
<table class="w-full text-sm text-left text-gray-500 table-fixed">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-4 w-12">
<div class="flex items-center">
<input id="checkbox-all-permanent" type="checkbox" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500">
<label for="checkbox-all-permanent" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-4 py-2 w-2/5">Message</th>
<th scope="col" class="px-4 py-2 w-1/6">Last seen</th>
<th scope="col" class="px-4 py-2 w-1/6">First seen</th>
<th scope="col" class="px-4 py-2 w-1/6">Count</th>
</tr>
</thead>
<tbody>
@foreach (var errorGroup in permanentlyIgnoredErrors)
{
@await Html.PartialAsync("_ErrorGroupRow", errorGroup)
}
</tbody>
</table>
</div>
</div>
</div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
const tabs = document.querySelectorAll('.status-tab:not([disabled])');
const contents = document.querySelectorAll('.status-content');

tabs.forEach(tab => {
tab.addEventListener('click', function() {
if (this.disabled) return;

const selectedStatus = this.dataset.status;

// Update active tab
tabs.forEach(t => {
t.classList.remove('active', 'bg-gray-100', 'font-bold', '!text-black');
t.classList.add('text-gray-500');
if (!t.disabled) {
t.classList.add('hover:bg-gray-50');
}
});
this.classList.add('active', 'bg-gray-100', 'font-bold', '!text-black');
this.classList.remove('hover:bg-gray-50', 'text-gray-500');

// Show/hide content sections
contents.forEach(content => {
if (content.dataset.status === selectedStatus) {
content.style.display = '';
} else {
content.style.display = 'none';
}
});
});
});
});
</script>
6 changes: 6 additions & 0 deletions src/Minigun/Areas/Home/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ public async Task<IActionResult> RegisterPat([FromForm] string patToken)
return new EmptyResult();
}

[HttpGet("/Error")]
public IActionResult Error()
{
return View();
}

private async Task<bool> IsValidPatToken(string patToken)
{
var client = _httpClientFactory.CreateClient();
Expand Down
20 changes: 20 additions & 0 deletions src/Minigun/Areas/Home/Views/Home/Error.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@{
ViewData["Title"] = "Error";
}

<div class="flex items-center justify-center min-h-screen bg-primary-navy-dark">
<div class="w-full max-w-md bg-white rounded-lg shadow-md p-8">
<h2 class="text-2xl font-bold mb-6 text-primary-navy-dark text-center">Something went wrong</h2>

<p class="text-center text-gray-600 mb-6">
An error occurred while processing your request.
</p>

<div class="text-center">
<a href="@Url.Action("Index", "Home", new { area = "Home" })"
class="inline-block bg-primary-raygun-blue text-white font-bold py-2 px-4 rounded-md hover:bg-blue-600 transition-colors duration-300">
Return Home
</a>
</div>
</div>
</div>
11 changes: 10 additions & 1 deletion src/Minigun/Areas/Home/Views/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
@{
var canonicalHost = "https://minigun.proredcat.xyz";
var path = Context?.Request?.Path.Value ?? string.Empty;
var canonicalUrl = $"{canonicalHost}{(string.IsNullOrEmpty(path) ? "/" : path)}";
}
<link rel="canonical" href="@canonicalUrl"/>
<title>Minigun - @ViewData["Title"]</title>
<script type="importmap"></script>
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
Expand All @@ -14,6 +20,9 @@
<main role="main" class="w-screen">
@RenderBody()
</main>
<footer class="fixed bottom-0 right-0 p-2 text-xs text-gray-500">
@await Component.InvokeAsync("RuntimeInfo")
</footer>
</div>
</body>
</html>
</html>
2 changes: 1 addition & 1 deletion src/Minigun/Areas/Shared/_ApplicationsPartial.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<div class="dropdown-item px-4 py-2 text-sm text-gray-700 cursor-pointer hover:bg-gray-100 @(application.Identifier == Model.SelectedApplicationId ? "bg-gray-100" : "")"
data-application-id="@application.Identifier"
data-application-name="@application.Name"
hx-on:click="selectApplication(this)">
onclick="selectApplication(this)">
@application.Name
</div>
}
Expand Down
Loading
Loading