Added process Log Back and Import CSV

Still working on fixing the malformed csv issue but we're getting really close. Process Log Back will re-import all kills that were not recorded to the api. This was added so that any kills that were missed during downtime can be re-submitted to the api.
This commit is contained in:
Heavy Bob 2025-06-10 14:28:30 +10:00
parent 316911ba7d
commit 0f97d758f8
7 changed files with 413 additions and 227 deletions

View file

@ -52,115 +52,115 @@
Margin="0,0,0,10"
VerticalAlignment="Top">
<StackPanel
VerticalAlignment="Top"
HorizontalAlignment="Center"
Width="152"
Margin="10,5,10,5">
<TextBlock Name="PilotNameTitle"
Text="Pilot"
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,5,0,0"
Foreground="{DynamicResource AltTextBrush}"
FontSize="14"/>
<TextBlock Name="PilotNameTextBox"
Text=""
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,0,0,0"
Foreground="{DynamicResource TextBrush}"
FontSize="10"
TextAlignment="Center"/>
<TextBlock Name="PlayerShipTitle"
Text="Ship"
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,5,0,0"
Foreground="{DynamicResource AltTextBrush}"
FontSize="14"/>
<TextBlock Name="PlayerShipTextBox"
Text=""
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,0,0,0"
Foreground="{DynamicResource TextBrush}"
FontSize="10"
TextAlignment="Center"/>
<TextBlock Name="GameModeTitle"
Text="Game Mode"
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,5,0,0"
Foreground="{DynamicResource AltTextBrush}"
FontSize="14"/>
<TextBlock Name="GameModeTextBox"
Text=""
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,0,0,0"
Foreground="{DynamicResource TextBrush}"
FontSize="10"
TextAlignment="Center"/>
<TextBlock Name="LocationTitle"
Text="Location"
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,5,0,0"
Foreground="{DynamicResource AltTextBrush}"
FontSize="14"/>
<TextBlock Name="LocationTextBox"
Text="Unknown"
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,0,0,0"
Foreground="{DynamicResource TextBrush}"
FontSize="10"
TextAlignment="Center"/>
<TextBlock Name="KillTallyTitle"
Text="Kill Tally"
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,5,0,0"
Foreground="{DynamicResource AltTextBrush}"
FontSize="14"/>
<TextBlock Name="KillTallyTextBox"
Text=""
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,0,0,0"
Foreground="{DynamicResource TextBrush}"
FontSize="10"
TextAlignment="Center"/>
<TextBox x:Name="DebugPanel"
Text=""
Width="152"
Height="98"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Foreground="{DynamicResource TextBrush}"
FontSize="8"
BorderThickness="0"
Margin="0,9,0,0"/>
VerticalAlignment="Top"
HorizontalAlignment="Center"
Width="152"
Margin="10,5,10,5">
<TextBlock Name="PilotNameTitle"
Text="Pilot"
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,5,0,0"
Foreground="{DynamicResource AltTextBrush}"
FontSize="14"/>
<TextBlock Name="PilotNameTextBox"
Text=""
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,0,0,0"
Foreground="{DynamicResource TextBrush}"
FontSize="10"
TextAlignment="Center"/>
<TextBlock Name="PlayerShipTitle"
Text="Ship"
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,5,0,0"
Foreground="{DynamicResource AltTextBrush}"
FontSize="14"/>
<TextBlock Name="PlayerShipTextBox"
Text=""
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,0,0,0"
Foreground="{DynamicResource TextBrush}"
FontSize="10"
TextAlignment="Center"/>
<TextBlock Name="GameModeTitle"
Text="Game Mode"
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,5,0,0"
Foreground="{DynamicResource AltTextBrush}"
FontSize="14"/>
<TextBlock Name="GameModeTextBox"
Text=""
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,0,0,0"
Foreground="{DynamicResource TextBrush}"
FontSize="10"
TextAlignment="Center"/>
<TextBlock Name="LocationTitle"
Text="Location"
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,5,0,0"
Foreground="{DynamicResource AltTextBrush}"
FontSize="14"/>
<TextBlock Name="LocationTextBox"
Text="Unknown"
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,0,0,0"
Foreground="{DynamicResource TextBrush}"
FontSize="10"
TextAlignment="Center"/>
<TextBlock Name="KillTallyTitle"
Text="Kill Tally"
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,5,0,0"
Foreground="{DynamicResource AltTextBrush}"
FontSize="14"/>
<TextBlock Name="KillTallyTextBox"
Text=""
Width="152"
Height="20"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Margin="0,0,0,0"
Foreground="{DynamicResource TextBrush}"
FontSize="10"
TextAlignment="Center"/>
<TextBox x:Name="DebugPanel"
Text=""
Width="152"
Height="98"
Background="Transparent"
FontFamily="{StaticResource Orbitron}"
Foreground="{DynamicResource TextBrush}"
FontSize="8"
BorderThickness="0"
Margin="0,9,0,0"/>
</StackPanel>
</Border>
<StackPanel Grid.Row="1"

View file

@ -21,14 +21,14 @@ public partial class HomePage : UserControl
private LogHandler? _logHandler;
private KillHistoryManager _killHistoryManager;
private LogBackupProcessor? _logBackupProcessor;
private bool _UIEventsRegistered = false;
private System.Timers.Timer _statusCheckTimer;
private bool _isLogHandlerRunning = false;
private int _counter = 1;
private System.Timers.Timer _counterTimer;
private bool _isInitializing = false;
private System.Timers.Timer? _initializationTimer;
private bool _wasStarCitizenRunningOnStart = false;
private bool _isProcessingLogBackups = false;
public HomePage()
{
@ -62,15 +62,16 @@ public partial class HomePage : UserControl
_statusCheckTimer = new System.Timers.Timer(1000); // Check every second
_statusCheckTimer.Elapsed += CheckStarCitizenStatus;
_statusCheckTimer.Start();
// Initialize and start the counter timer
_counterTimer = new System.Timers.Timer(1000); // Update every second
_counterTimer.Elapsed += UpdateCounter;
_counterTimer.Start();
}
private void CheckStarCitizenStatus(object? sender, ElapsedEventArgs e)
{
if (_isProcessingLogBackups)
{
// Simulate TrackR as running during log backup processing
Dispatcher.Invoke(() => UpdateStatusIndicator(true));
return;
}
bool isRunning = IsStarCitizenRunning();
Dispatcher.Invoke(() =>
{
@ -569,12 +570,40 @@ public partial class HomePage : UserControl
return Process.GetProcessesByName("StarCitizen").Length > 0;
}
private void UpdateCounter(object? sender, ElapsedEventArgs e)
public async void ProcessLogBackups_Click(object sender, RoutedEventArgs e)
{
Dispatcher.Invoke(() =>
if (_isProcessingLogBackups)
{
DebugPanel.Text = _counter.ToString();
_counter = (_counter % 10) + 1; // Count from 1 to 10 and loop
});
MessageBox.Show("Already processing log backups. Please wait.", "Processing", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
try
{
_isProcessingLogBackups = true;
UpdateStatusIndicator(true, true); // Set to yellow for processing
if (_logBackupProcessor == null)
{
var logBackupsPath = Path.Combine(Path.GetDirectoryName(ConfigManager.LogFile)!, "logbackups");
_logBackupProcessor = new LogBackupProcessor(logBackupsPath, _killHistoryManager, _logHandler?.GetEventHandlers() ?? new List<ILogEventHandler>());
}
await _logBackupProcessor.ProcessLogBackupsAsync((logFile) =>
{
DebugPanel.Text = $"Processing: {Path.GetFileName(logFile)}";
});
MessageBox.Show("Log backups processed successfully!", "Success", MessageBoxButton.OK, MessageBoxImage.Information);
DebugPanel.Text = ""; // Clear the debug panel after successful processing
}
catch (Exception ex)
{
MessageBox.Show($"Error processing log backups: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
_isProcessingLogBackups = false;
UpdateStatusIndicator(IsStarCitizenRunning());
}
}
}

View file

@ -2,117 +2,132 @@
using System.IO;
using System.Text;
using System.Linq;
using System.Diagnostics;
using System.Threading;
using System.Collections.Concurrent;
using System.Threading.Tasks;
namespace AutoTrackR2;
public class KillHistoryManager
{
private string _killHistoryPath;
private readonly string _killHistoryPath;
private readonly string _headers = "KillTime,EnemyPilot,EnemyShip,Enlisted,RecordNumber,OrgAffiliation,Player,Weapon,Ship,Method,Mode,GameVersion,TrackRver,Logged,PFP,Hash\n";
private readonly KillStreakManager _killStreakManager;
private readonly ConcurrentQueue<KillData> _killQueue;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly Task _processingTask;
private bool _killStreakSoundEnabled = true;
public KillHistoryManager(string logPath, string soundsPath)
{
_killHistoryPath = logPath;
_killStreakManager = new KillStreakManager(soundsPath);
var appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AutoTrackR2");
Directory.CreateDirectory(appDataPath); // Ensure the directory exists
_killHistoryPath = Path.Combine(appDataPath, "Kill-log.csv");
// Create the CSV file with headers if it doesn't exist
if (!File.Exists(_killHistoryPath))
{
File.WriteAllText(_killHistoryPath, _headers);
}
else
_killStreakManager = new KillStreakManager(soundsPath);
_killQueue = new ConcurrentQueue<KillData>();
_cancellationTokenSource = new CancellationTokenSource();
// Start the background processing task
_processingTask = Task.Run(ProcessKillQueue);
}
private async Task ProcessKillQueue()
{
while (!_cancellationTokenSource.Token.IsCancellationRequested)
{
CheckAndFixMalformedCsv();
try
{
if (_killQueue.TryDequeue(out var kill))
{
await ProcessKillAsync(kill);
}
else
{
await Task.Delay(100, _cancellationTokenSource.Token);
}
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Debug.WriteLine($"Error processing kill: {ex.Message}");
await Task.Delay(1000, _cancellationTokenSource.Token);
}
}
}
private void CheckAndFixMalformedCsv()
private async Task ProcessKillAsync(KillData kill)
{
try
{
// Try to read the file to check if it's malformed
using var reader = new StreamReader(_killHistoryPath);
var firstLine = reader.ReadLine();
// If the file is empty or doesn't start with the correct headers, it's malformed
if (string.IsNullOrEmpty(firstLine) || firstLine != _headers.TrimEnd('\n'))
// Ensure all fields are properly escaped for CSV
var fields = new[]
{
// Create a backup of the malformed file
string backupPath = Path.Combine(
Path.GetDirectoryName(_killHistoryPath)!,
"Kill-log.old"
);
kill.KillTime.ToString(),
EscapeCsvField(kill.EnemyPilot),
EscapeCsvField(kill.EnemyShip),
EscapeCsvField(kill.Enlisted),
EscapeCsvField(kill.RecordNumber),
EscapeCsvField(kill.OrgAffiliation),
EscapeCsvField(kill.Player),
EscapeCsvField(kill.Weapon),
EscapeCsvField(kill.Ship),
EscapeCsvField(kill.Method),
EscapeCsvField(kill.Mode),
EscapeCsvField(kill.GameVersion),
EscapeCsvField(kill.TrackRver),
EscapeCsvField(kill.Logged),
EscapeCsvField(kill.PFP),
EscapeCsvField(kill.Hash)
};
// If Kill-log.old already exists, delete it
if (File.Exists(backupPath))
{
File.Delete(backupPath);
}
var csvLine = string.Join(",", fields);
// Rename the malformed file
File.Move(_killHistoryPath, backupPath);
// Create a new file with correct headers
File.WriteAllText(_killHistoryPath, _headers);
}
// Use FileShare.Read to allow other processes to read while we write
using var stream = new FileStream(_killHistoryPath, FileMode.Append, FileAccess.Write, FileShare.Read);
using var writer = new StreamWriter(stream);
await writer.WriteLineAsync(csvLine);
}
catch (Exception ex)
{
// If there's any error reading the file, consider it malformed
Console.WriteLine($"Error reading CSV file: {ex.Message}");
string backupPath = Path.Combine(
Path.GetDirectoryName(_killHistoryPath)!,
"Kill-log.old"
);
// If Kill-log.old already exists, delete it
if (File.Exists(backupPath))
{
File.Delete(backupPath);
}
// Rename the malformed file
File.Move(_killHistoryPath, backupPath);
// Create a new file with correct headers
File.WriteAllText(_killHistoryPath, _headers);
Debug.WriteLine($"Error writing kill to CSV: {ex.Message}");
throw;
}
}
public void AddKill(KillData killData)
private string EscapeCsvField(string field)
{
// Ensure the CSV file exists
// This should only happen if the file was deleted or corrupted
if (!File.Exists(_killHistoryPath))
{
File.WriteAllText(_killHistoryPath, _headers);
}
// Remove comma from Enlisted
killData.Enlisted = killData.Enlisted?.Replace(",", string.Empty);
// Append the new kill data to the CSV file
var csv = new StringBuilder();
csv.AppendLine($"\"{killData.KillTime}\",\"{killData.EnemyPilot}\",\"{killData.EnemyShip}\",\"{killData.Enlisted}\",\"{killData.RecordNumber}\",\"{killData.OrgAffiliation}\",\"{killData.Player}\",\"{killData.Weapon}\",\"{killData.Ship}\",\"{killData.Method}\",\"{killData.Mode}\",\"{killData.GameVersion}\",\"{killData.TrackRver}\",\"{killData.Logged}\",\"{killData.PFP}\",\"{killData.Hash}\"");
if (string.IsNullOrEmpty(field)) return "";
// Check file can be written to
try
// If the field contains any special characters, wrap it in quotes
if (field.Contains(",") || field.Contains("\"") || field.Contains("\n") || field.Contains("\r"))
{
using var fileStream = new FileStream(_killHistoryPath, FileMode.Append, FileAccess.Write, FileShare.Read);
using var writer = new StreamWriter(fileStream);
writer.Write(csv.ToString());
// Double up any quotes
field = field.Replace("\"", "\"\"");
return $"\"{field}\"";
}
// Trigger kill streak sound only if enabled
if (ConfigManager.KillStreakEnabled == 1)
{
_killStreakManager.OnKill();
}
}
catch (IOException ex)
{
// Handle the exception (e.g., log it)
Console.WriteLine($"Error writing to file: {ex.Message}");
}
return field;
}
public void AddKill(KillData kill)
{
_killQueue.Enqueue(kill);
}
public void PlayKillStreakSound()
{
_killStreakManager.OnKill();
}
public void ResetKillStreak()
@ -120,23 +135,30 @@ public class KillHistoryManager
_killStreakManager.OnDeath();
}
public void Dispose()
{
_cancellationTokenSource.Cancel();
_processingTask.Wait();
_cancellationTokenSource.Dispose();
}
public List<KillData> GetKills()
{
var kills = new List<KillData>();
using var reader = new StreamReader(new FileStream(_killHistoryPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite));
reader.ReadLine(); // Skip headers
while (reader.Peek() >= 0)
{
var line = reader.ReadLine();
// Remove extra quotes from CSV data
// Todo: These quotes are for handling commas in the data, but not sure if they're necessary
line = line?.Replace("\"", string.Empty);
var data = line?.Split(',');
kills.Add(new KillData
{
KillTime = data?[0],

View file

@ -0,0 +1,83 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using AutoTrackR2.LogEventHandlers;
using System.Windows;
using System.Linq;
using System.Threading;
namespace AutoTrackR2;
public class LogBackupProcessor
{
private readonly string _logBackupsPath;
private readonly KillHistoryManager _killHistoryManager;
private readonly List<ILogEventHandler> _logEventHandlers;
public LogBackupProcessor(string logBackupsPath, KillHistoryManager killHistoryManager, List<ILogEventHandler> logEventHandlers)
{
_logBackupsPath = logBackupsPath;
_killHistoryManager = killHistoryManager;
_logEventHandlers = logEventHandlers;
}
public async Task ProcessLogBackupsAsync(Action<string>? onLogFileProcessed = null)
{
if (!Directory.Exists(_logBackupsPath))
{
Console.WriteLine($"Log backups directory not found: {_logBackupsPath}");
return;
}
var logFiles = Directory.GetFiles(_logBackupsPath, "*.log", SearchOption.AllDirectories)
.Where(file => File.GetLastWriteTime(file) >= new DateTime(2025, 3, 27))
.ToArray();
Array.Sort(logFiles); // Process files in chronological order
for (int i = 0; i < logFiles.Length; i++)
{
var logFile = logFiles[i];
onLogFileProcessed?.Invoke(logFile);
await ProcessLogFileAsync(logFile);
}
}
private async Task ProcessLogFileAsync(string logFilePath)
{
try
{
using var reader = new StreamReader(logFilePath);
string? line;
var lines = new List<string>();
while ((line = await reader.ReadLineAsync()) != null)
{
lines.Add(line);
}
int actorDeathCount = 0;
await Task.Run(() => Parallel.ForEach(lines, line =>
{
var entry = new LogEntry { Message = line };
foreach (var handler in _logEventHandlers)
{
if (handler.Pattern.IsMatch(line))
{
handler.Handle(entry);
if (handler is ActorDeathEvent)
{
Interlocked.Increment(ref actorDeathCount);
}
}
}
}));
Console.WriteLine($"Processed {actorDeathCount} actor deaths in {logFilePath}");
// Wait 5 seconds after processing the file before moving to the next file
await Task.Delay(TimeSpan.FromSeconds(5));
}
catch (Exception ex)
{
Console.WriteLine($"Error processing log file {logFilePath}: {ex.Message}");
}
}
}

View file

@ -46,9 +46,10 @@ public class LogHandler
new GameVersionEvent(),
new JumpDriveStateChangedEvent(),
new RequestJumpFailedEvent(),
new VehicleDestructionEvent()
new VehicleDestructionEvent(),
new ActorDeathEvent()
];
public LogHandler(string? logPath)
{
if (string.IsNullOrEmpty(logPath))
@ -104,8 +105,6 @@ public class LogHandler
HandleLogEntry(line);
}
// Ensures that any deaths already in log aren't sent to the APIs until the monitor thread is running
_eventHandlers.Add(new ActorDeathEvent());
StartMonitoring();
}
@ -218,4 +217,9 @@ public class LogHandler
}
_gameProcessState = newGameProcessState;
}
public List<ILogEventHandler> GetEventHandlers()
{
return new List<ILogEventHandler>(_eventHandlers);
}
}

View file

@ -1,37 +1,80 @@
<Window x:Class="AutoTrackR2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="AutoTrackR" Height="450" Width="800"
WindowStyle="None" ResizeMode="NoResize"
Title="AutoTrackR"
Height="450"
Width="800"
WindowStyle="None"
ResizeMode="NoResize"
AllowsTransparency="True"
Style="{StaticResource CustomWindowStyle}">
<Grid>
<!-- Custom Title Bar -->
<DockPanel Height="30" VerticalAlignment="Top" MouseDown="TitleBar_MouseDown" Margin="5" Background="Transparent">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="_" Width="30" Height="25" Click="MinimizeWindow" Style="{StaticResource TitleButtonStyle}" FontFamily="{StaticResource Orbitron}"/>
<Button Content="X" Width="30" Height="25" Click="CloseWindow" Style="{StaticResource TitleButtonStyle}" FontFamily="{StaticResource Orbitron}"/>
<DockPanel Height="30"
VerticalAlignment="Top"
MouseDown="TitleBar_MouseDown"
Margin="5"
Background="Transparent">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Content="_"
Width="30"
Height="25"
Click="MinimizeWindow"
Style="{StaticResource TitleButtonStyle}"
FontFamily="{StaticResource Orbitron}"/>
<Button Content="X"
Width="30"
Height="25"
Click="CloseWindow"
Style="{StaticResource TitleButtonStyle}"
FontFamily="{StaticResource Orbitron}"/>
</StackPanel>
</DockPanel>
<!-- Main Content Area -->
<Grid Margin="0,30,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Margin="0,30,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Left Tab Panel -->
<StackPanel VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="5,0,0,0">
<Image x:Name="Logo" Height="138" Source="/Assets/AutoTrackR.png" Stretch="Fill" Width="141" RenderOptions.BitmapScalingMode="Fant"/>
<Button Content="Home" Name="HomeTab" Margin="10,40,10,10" Height="40" Style="{StaticResource TabButtonStyle}" Click="TabButton_Click"/>
<Button Content="Config" Name="ConfigTab" Margin="10" Height="40" Style="{StaticResource TabButtonStyle}" Click="TabButton_Click"/>
</StackPanel>
<!-- Left Tab Panel -->
<StackPanel VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Margin="5,0,0,0">
<Image x:Name="Logo"
Height="138"
Source="/Assets/AutoTrackR.png"
Stretch="Fill"
Width="141"
RenderOptions.BitmapScalingMode="Fant"/>
<Button Content="Home"
Name="HomeTab"
Margin="10,40,10,10"
Height="40"
Style="{StaticResource TabButtonStyle}"
Click="TabButton_Click"/>
<Button Content="Config"
Name="ConfigTab"
Margin="10"
Height="40"
Style="{StaticResource TabButtonStyle}"
Click="TabButton_Click"/>
<Button Content="Process Log Backups"
Name="ProcessLogBackupsButton"
Margin="10"
Height="40"
Style="{StaticResource TabButtonStyle}"
Click="ProcessLogBackups_Click"/>
</StackPanel>
<!-- Content Area -->
<ContentControl Grid.Column="1" Name="ContentControl" Margin="10">
<!-- Default content can be set here -->
</ContentControl>
</Grid>
<!-- Content Area -->
<ContentControl Grid.Column="1"
Name="ContentControl"
Margin="10">
<!-- Default content can be set here -->
</ContentControl>
</Grid>
</Grid>
</Window>

View file

@ -120,6 +120,11 @@ namespace AutoTrackR2
UpdateTabVisuals();
}
private void ProcessLogBackups_Click(object sender, RoutedEventArgs e)
{
homePage.ProcessLogBackups_Click(sender, e);
}
private void UpdateTabStates(string activeTab)
{
foreach (var key in tabStates.Keys)