diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 640a892f8..785d52119 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -42,8 +42,8 @@ jobs:
release_name_cli="ErsatzTV.CommandLine-$tag-${{ matrix.target }}"
# Build everything
- dotnet publish ErsatzTV/ErsatzTV.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name"
- dotnet publish ErsatzTV.CommandLine/ErsatzTV.CommandLine.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name_cli"
+ dotnet publish ErsatzTV/ErsatzTV.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}"
+ dotnet publish ErsatzTV.CommandLine/ErsatzTV.CommandLine.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name_cli" /property:InformationalVersion="${tag:1}-${{ matrix.target }}"
# Pack files
if [ "${{ matrix.target }}" == "win-x64" ]; then
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 000000000..c4d1aa6eb
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,5 @@
+
+
+ develop
+
+
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index fd5e6414e..db895e8c0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -26,7 +26,8 @@ COPY ErsatzTV.Core/. ./ErsatzTV.Core/
COPY ErsatzTV.Core.Tests/. ./ErsatzTV.Core.Tests/
COPY ErsatzTV.Infrastructure/. ./ErsatzTV.Infrastructure/
WORKDIR /source/ErsatzTV
-RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore
+ARG INFO_VERSION="unknown"
+RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore /p:InformationalVersion=${INFO_VERSION}
# final stage/image
FROM runtime-base
diff --git a/ErsatzTV.Application/Logs/LogEntryViewModel.cs b/ErsatzTV.Application/Logs/LogEntryViewModel.cs
new file mode 100644
index 000000000..0f81c11ed
--- /dev/null
+++ b/ErsatzTV.Application/Logs/LogEntryViewModel.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace ErsatzTV.Application.Logs
+{
+ public record LogEntryViewModel(
+ int Id,
+ DateTime Timestamp,
+ string Level,
+ string Exception,
+ string RenderedMessage,
+ string Properties);
+}
diff --git a/ErsatzTV.Application/Logs/Mapper.cs b/ErsatzTV.Application/Logs/Mapper.cs
new file mode 100644
index 000000000..7d9055b81
--- /dev/null
+++ b/ErsatzTV.Application/Logs/Mapper.cs
@@ -0,0 +1,16 @@
+using ErsatzTV.Core.Domain;
+
+namespace ErsatzTV.Application.Logs
+{
+ internal static class Mapper
+ {
+ internal static LogEntryViewModel ProjectToViewModel(LogEntry logEntry) =>
+ new(
+ logEntry.Id,
+ logEntry.Timestamp,
+ logEntry.Level,
+ logEntry.Exception,
+ logEntry.RenderedMessage,
+ logEntry.Properties);
+ }
+}
diff --git a/ErsatzTV.Application/Logs/Queries/GetRecentLogEntries.cs b/ErsatzTV.Application/Logs/Queries/GetRecentLogEntries.cs
new file mode 100644
index 000000000..0b92744d7
--- /dev/null
+++ b/ErsatzTV.Application/Logs/Queries/GetRecentLogEntries.cs
@@ -0,0 +1,7 @@
+using System.Collections.Generic;
+using MediatR;
+
+namespace ErsatzTV.Application.Logs.Queries
+{
+ public record GetRecentLogEntries : IRequest>;
+}
diff --git a/ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs b/ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs
new file mode 100644
index 000000000..6997a3de3
--- /dev/null
+++ b/ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using ErsatzTV.Core.Interfaces.Repositories;
+using LanguageExt;
+using MediatR;
+using static ErsatzTV.Application.Logs.Mapper;
+
+namespace ErsatzTV.Application.Logs.Queries
+{
+ public class GetRecentLogEntriesHandler : IRequestHandler>
+ {
+ private readonly ILogRepository _logRepository;
+
+ public GetRecentLogEntriesHandler(ILogRepository logRepository) => _logRepository = logRepository;
+
+ public Task> Handle(GetRecentLogEntries request, CancellationToken cancellationToken) =>
+ _logRepository.GetRecentLogEntries().Map(list => list.Map(ProjectToViewModel).ToList());
+ }
+}
diff --git a/ErsatzTV.Core/Domain/LogEntry.cs b/ErsatzTV.Core/Domain/LogEntry.cs
new file mode 100644
index 000000000..6685e8341
--- /dev/null
+++ b/ErsatzTV.Core/Domain/LogEntry.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace ErsatzTV.Core.Domain
+{
+ public record LogEntry(
+ int Id,
+ DateTime Timestamp,
+ string Level,
+ string Exception,
+ string RenderedMessage,
+ string Properties);
+}
diff --git a/ErsatzTV.Core/FileSystemLayout.cs b/ErsatzTV.Core/FileSystemLayout.cs
index 5e2f17cf9..258c619c9 100644
--- a/ErsatzTV.Core/FileSystemLayout.cs
+++ b/ErsatzTV.Core/FileSystemLayout.cs
@@ -13,6 +13,8 @@ namespace ErsatzTV.Core
public static readonly string DatabasePath = Path.Combine(AppDataFolder, "ersatztv.sqlite3");
+ public static readonly string LogDatabasePath = Path.Combine(AppDataFolder, "logs.sqlite3");
+
public static readonly string ImageCacheFolder = Path.Combine(AppDataFolder, "cache", "images");
public static readonly string PlexSecretsPath = Path.Combine(AppDataFolder, "plex-secrets.json");
diff --git a/ErsatzTV.Core/Interfaces/Repositories/ILogRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/ILogRepository.cs
new file mode 100644
index 000000000..096d24be6
--- /dev/null
+++ b/ErsatzTV.Core/Interfaces/Repositories/ILogRepository.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using ErsatzTV.Core.Domain;
+
+namespace ErsatzTV.Core.Interfaces.Repositories
+{
+ public interface ILogRepository
+ {
+ public Task> GetRecentLogEntries();
+ }
+}
diff --git a/ErsatzTV.Infrastructure/Data/Configurations/LogEntryConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/LogEntryConfiguration.cs
new file mode 100644
index 000000000..e79223e0d
--- /dev/null
+++ b/ErsatzTV.Infrastructure/Data/Configurations/LogEntryConfiguration.cs
@@ -0,0 +1,12 @@
+using ErsatzTV.Core.Domain;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ErsatzTV.Infrastructure.Data.Configurations
+{
+ public class LogEntryConfiguration : IEntityTypeConfiguration
+ {
+ public void Configure(EntityTypeBuilder builder) =>
+ builder.ToTable("Logs");
+ }
+}
diff --git a/ErsatzTV.Infrastructure/Data/LogContext.cs b/ErsatzTV.Infrastructure/Data/LogContext.cs
new file mode 100644
index 000000000..875bf84bd
--- /dev/null
+++ b/ErsatzTV.Infrastructure/Data/LogContext.cs
@@ -0,0 +1,21 @@
+using ErsatzTV.Core.Domain;
+using Microsoft.EntityFrameworkCore;
+
+namespace ErsatzTV.Infrastructure.Data
+{
+ public class LogContext : DbContext
+ {
+ public LogContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ public DbSet LogEntries { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ base.OnModelCreating(builder);
+ builder.ApplyConfigurationsFromAssembly(typeof(LogContext).Assembly);
+ }
+ }
+}
diff --git a/ErsatzTV.Infrastructure/Data/Repositories/LogRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/LogRepository.cs
new file mode 100644
index 000000000..7d4864ce1
--- /dev/null
+++ b/ErsatzTV.Infrastructure/Data/Repositories/LogRepository.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using ErsatzTV.Core.Domain;
+using ErsatzTV.Core.Interfaces.Repositories;
+using Microsoft.EntityFrameworkCore;
+
+namespace ErsatzTV.Infrastructure.Data.Repositories
+{
+ public class LogRepository : ILogRepository
+ {
+ private readonly LogContext _logContext;
+
+ public LogRepository(LogContext logContext) => _logContext = logContext;
+
+ public Task> GetRecentLogEntries() =>
+ _logContext.LogEntries.OrderByDescending(e => e.Id).Take(100).ToListAsync();
+ }
+}
diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj
index 890d128ad..1b97f7cad 100644
--- a/ErsatzTV/ErsatzTV.csproj
+++ b/ErsatzTV/ErsatzTV.csproj
@@ -25,6 +25,7 @@
+
diff --git a/ErsatzTV/Pages/Logs.razor b/ErsatzTV/Pages/Logs.razor
new file mode 100644
index 000000000..23e43dce1
--- /dev/null
+++ b/ErsatzTV/Pages/Logs.razor
@@ -0,0 +1,29 @@
+@page "/system/logs"
+@using ErsatzTV.Application.Logs
+@using ErsatzTV.Application.Logs.Queries
+@inject IMediator Mediator
+
+
+
+ Timestamp
+ Level
+ Message
+ Properties
+
+
+ @context.Timestamp
+ @context.Level
+ @context.RenderedMessage
+ @context.Properties
+
+
+
+
+
+
+@code {
+ private List _logEntries;
+
+ protected override async Task OnInitializedAsync() => _logEntries = await Mediator.Send(new GetRecentLogEntries());
+
+}
\ No newline at end of file
diff --git a/ErsatzTV/Program.cs b/ErsatzTV/Program.cs
index 517746095..b7c7d5fe8 100644
--- a/ErsatzTV/Program.cs
+++ b/ErsatzTV/Program.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Threading.Tasks;
+using ErsatzTV.Core;
using ErsatzTV.Extensions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
@@ -25,6 +26,7 @@ namespace ErsatzTV
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(Configuration)
.Enrich.FromLogContext()
+ .WriteTo.SQLite(FileSystemLayout.LogDatabasePath, retentionPeriod: TimeSpan.FromDays(1))
.CreateLogger();
try
diff --git a/ErsatzTV/Shared/MainLayout.razor b/ErsatzTV/Shared/MainLayout.razor
index e348ed1ab..efac1ed82 100644
--- a/ErsatzTV/Shared/MainLayout.razor
+++ b/ErsatzTV/Shared/MainLayout.razor
@@ -1,4 +1,5 @@
-@inherits LayoutComponentBase
+@using System.Reflection
+@inherits LayoutComponentBase
@@ -32,6 +33,12 @@
Schedules
Playouts
+ Logs
+
+
+ ErsatzTV Version
+ @InfoVersion
+
@@ -42,7 +49,9 @@
@code {
- bool _drawerOpen = true;
+ private static readonly string InfoVersion = Assembly.GetEntryAssembly().GetCustomAttribute()?.InformationalVersion ?? "unknown";
+
+ private bool _drawerOpen = true;
private void DrawerToggle() => _drawerOpen = !_drawerOpen;
diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs
index 7ae4b9cee..e4403bdbd 100644
--- a/ErsatzTV/Startup.cs
+++ b/ErsatzTV/Startup.cs
@@ -108,6 +108,9 @@ namespace ErsatzTV
o.MigrationsAssembly("ErsatzTV.Infrastructure");
}));
+ services.AddDbContext(
+ options => options.UseSqlite($"Data Source={FileSystemLayout.LogDatabasePath}"));
+
services.AddMediatR(typeof(GetAllChannels).Assembly);
services.AddRefitClient()
@@ -156,6 +159,7 @@ namespace ErsatzTV
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
diff --git a/docker-compose.yml b/docker-compose.yml
index b9b5eec39..7340deb27 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,6 +4,8 @@ services:
ersatztv:
build:
context: .
+ args:
+ INFO_VERSION: "docker-compose-develop"
ports:
- "8409:8409"
volumes: