feat: expand cockpit integrations and support workflows

Add Phoenix remote desktop, action connector, documentation engine, and gamification support. Refactor support-case processing and search/action UI, and update installer assets and dependencies.
This commit is contained in:
Meik
2026-07-13 11:16:53 +02:00
parent bbedbf71c8
commit 0b52999b1d
303 changed files with 11235 additions and 3580 deletions

View File

@@ -0,0 +1,13 @@
namespace F4SD.Gamification
{
public enum CockpitAction
{
CaseOpened,
CaseClosed,
QuickActionExecuted,
CopyTemplateClicked,
BuiltDirectConnection,
AddNotes,
StartRemoteConnection,
}
}

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>F4SD.Gamification</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<LangVersion>9.0</LangVersion>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,10 @@
namespace F4SD.Gamification
{
public class LevelEventArgs
{
public int CurrentLevel { get; set; }
public string LevelTitle { get; set; }
public int CurrentXp { get; set; }
public int XpRequiredForLevelUp { get; set; }
}
}

View File

@@ -0,0 +1,25 @@
using System;
namespace F4SD.Gamification.Services
{
public static class GamificationService
{
private static readonly LevelService _levelService = new();
public static void Initialize()
{
PersistenceService.Initialize();
foreach (var action in PersistenceService.GetActions())
{
_levelService.InitializeAction(action.Action);
}
}
public static void TrackAction(CockpitAction action, params object[] args)
{
PersistenceService.Persist(action);
_levelService.TrackAction(action);
}
}
}

View File

@@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Security.Cryptography;
namespace F4SD.Gamification.Services
{
public class LevelService
{
private static readonly List<(int MinimumTotalExperiencePoints, int Level)> _experienceToLevelMap = new()
{
(0,1),
(1128,2),
(2400,3),
(3832,4),
(5440,5),
(7240,6),
(9248,7),
(11480,8),
(13952,9),
(16680,10),
(19680,11),
(22968,12),
(26560,13),
(30472,14),
(34720,15),
(39320,16),
(44288,17),
(49640,18),
(55392,19),
(61560,20),
(68160,21),
(75208,22),
(82720,23),
(90712,24),
(99200,25),
(108200,26),
(117728,27),
(127800,28),
(138432,29),
(149640,30),
(161440,31),
(173848,32),
(186880,33),
(200552,34),
(214880,35),
(229880,36),
(245568,37),
(261960,38),
(279072,39),
(296920,40),
(315520,41),
(334888,42),
(355040,43),
(375992,44),
(397760,45),
(420360,46),
(443808,47),
(468120,48),
(493312,49),
(519400,50),
};
private int _totalExperiencePoints = 0;
private int _currentExperiencePoints;
private int _level;
internal void InitializeAction(CockpitAction action)
{
AddExperiencePoints(GetExperiencePointsFor(action), true);
}
internal void TrackAction(CockpitAction type, params object[] args)
{
AddExperiencePoints(GetExperiencePointsFor(type));
}
private void AddExperiencePoints(int points, bool isInitialization = false)
{
_totalExperiencePoints += points;
UpdateLevel(isInitialization);
UpdateCurrentExperiencePoints();
if (isInitialization)
return;
ExperiencePointsChanged?.Invoke(null, new LevelEventArgs()
{
CurrentLevel = _level,
LevelTitle = GetTitle(_level),
CurrentXp = _currentExperiencePoints,
XpRequiredForLevelUp = GetXpRequiredForNextLevel()
});
}
private void UpdateCurrentExperiencePoints()
{
int xpForCurrentLevel = _experienceToLevelMap.FirstOrDefault(mapEntry => mapEntry.Level == _level).MinimumTotalExperiencePoints;
_currentExperiencePoints = _totalExperiencePoints - xpForCurrentLevel;
}
private void UpdateLevel(bool isInitialization)
{
int previousLevel = _level;
_level = _experienceToLevelMap.FirstOrDefault(mapEntry => _totalExperiencePoints < mapEntry.MinimumTotalExperiencePoints).Level - 1;
if (!isInitialization && previousLevel < _level)
LevelChanged?.Invoke(null, new LevelEventArgs() { CurrentLevel = _level, LevelTitle = GetTitle(_level) });
}
private int GetXpRequiredForNextLevel()
{
int totalXpForNextLevel = _experienceToLevelMap.FirstOrDefault(mapEntry => mapEntry.Level == _level + 1).MinimumTotalExperiencePoints;
int totalXpForCurrentLevel = _experienceToLevelMap.FirstOrDefault(mapEntry => mapEntry.Level == _level).MinimumTotalExperiencePoints;
return totalXpForNextLevel - totalXpForCurrentLevel;
}
private int GetExperiencePointsFor(CockpitAction action)
{
switch (action)
{
case CockpitAction.CaseOpened:
break;
case CockpitAction.CaseClosed:
return 10;
case CockpitAction.QuickActionExecuted:
return 20;
case CockpitAction.CopyTemplateClicked:
return 5;
case CockpitAction.BuiltDirectConnection:
break;
case CockpitAction.AddNotes:
break;
case CockpitAction.StartRemoteConnection:
return 50;
default:
break;
}
return 1;
}
private static string GetTitle(int level)
{
switch (level)
{
case <= 5:
return "Support Trainee";
case <= 10:
return "Response Assistant";
case <= 15:
return "Service Aider";
case <= 20:
return "Ticket Resolver";
case <= 25:
return "Customer Navigator";
case <= 30:
return "Incident Coordinator";
case <= 35:
return "IT Guy";
case <= 40:
return "Support Specialist";
case <= 45:
return "Service Mentor";
case <= 49:
return "Operations Lead";
case > 49:
return "Ticket Titan";
}
}
public static event EventHandler<LevelEventArgs> ExperiencePointsChanged;
public static event EventHandler<LevelEventArgs> LevelChanged;
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace F4SD.Gamification.Services
{
internal static class PersistenceService
{
private static readonly string _databaseName = "F4SD-gmfc.txt";
private static readonly string _directory = $@"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Consulting4IT GmbH\C4IT First Aid Service Desk";
private static string GetConnectionString()
{
return Path.Combine(_directory, _databaseName);
}
internal static void Persist(CockpitAction action)
{
Initialize();
using var writer = File.AppendText(GetConnectionString());
string actionLine = $"{DateTime.UtcNow},{action}";
string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(actionLine));
writer.WriteLine(base64);
}
internal static void Initialize()
{
Directory.CreateDirectory(_directory);
if (!File.Exists(GetConnectionString()))
{
FileStream createdFile = File.Create(GetConnectionString());
File.SetAttributes(GetConnectionString(), FileAttributes.Hidden);
createdFile.Close();
}
}
internal static IEnumerable<(DateTime Time, CockpitAction Action)> GetActions()
{
using StreamReader reader = new(GetConnectionString());
string line;
while ((line = reader.ReadLine()) != null)
{
string encodedLine = string.Empty;
try { encodedLine = Encoding.UTF8.GetString(Convert.FromBase64String(line)); }
catch { continue; }
if (string.IsNullOrWhiteSpace(encodedLine))
continue;
string[] splittedLine = encodedLine.Split(',');
if (splittedLine.Length < 2)
continue;
if (!DateTime.TryParse(splittedLine[0], out var date))
continue;
if (!(Enum.TryParse(splittedLine[1], out CockpitAction action)))
continue;
yield return new(date, action);
}
}
}
}