diff --git a/.gitea/workflows/update-profile.yml b/.gitea/workflows/update-profile.yml
new file mode 100644
index 0000000..85c8f4d
--- /dev/null
+++ b/.gitea/workflows/update-profile.yml
@@ -0,0 +1,65 @@
+name: Update Gitea Profile
+
+on:
+ schedule:
+ # Täglich um 00:00 Uhr UTC
+ - cron: '0 0 * * *'
+ # Erlaubt das manuelle Ausführen über das Gitea Web-Interface
+ workflow_dispatch:
+ # Optional: Auch ausführen, wenn manuelle Änderungen an Code oder README gepusht werden
+ push:
+ branches:
+ - main
+ paths-ignore:
+ - 'heatmap.svg'
+ - 'README.md'
+
+jobs:
+ update-profile:
+ runs-on: ubuntu-latest
+
+ # Berechtigungen für das Schreiben ins Repository
+ permissions:
+ contents: write
+
+ steps:
+ - name: 📥 Repository auschecken
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ # Nutzt standardmäßig das automatische Gitea-Token oder ein benutzerdefiniertes Secret
+ token: ${{ secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN }}
+
+ - name: ⚙️ .NET 10 SDK einrichten
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: 🚀 C# Profile-Manager ausführen
+ env:
+ GITEA_API_URL: 'https://git.webcore-consulting.dev/api/v1'
+ GITEA_USERNAME: 'NickiCloud'
+ GITEA_TOKEN: ${{ secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN }}
+ run: |
+ dotnet run --project src/ProfileManager
+
+ - name: 📝 Änderungen prüfen, committen und pushen
+ run: |
+ # Git-Identität für den Bot konfigurieren
+ git config --global user.name "NickiBot[bot]"
+ git config --global user.email "bot@webcore-consulting.dev"
+
+ # Prüfen, ob sich heatmap.svg oder README.md geändert haben
+ if git status --porcelain | grep -E "(heatmap.svg|README.md)"; then
+ echo "Änderungen an heatmap.svg oder README.md erkannt. Committe..."
+ git add heatmap.svg README.md
+
+ # GANZ WICHTIG: [skip ci] verhindert Endlosschleifen in Gitea Actions!
+ git commit -m "chore(profile): update activity heatmap & stats [skip ci]"
+
+ # Änderungen pushen
+ git push
+ echo "Profil erfolgreich aktualisiert und gepusht! ✨"
+ else
+ echo "Keine Änderungen vorhanden. Alles auf dem neuesten Stand. 👍"
+ fi
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0e46e37
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+## .NET Build & Output
+bin/
+obj/
+*.user
+*.suo
+*.userosscache
+*.sln.docstates
+
+## IDEs
+.idea/
+.vscode/
+*.swp
+
+## OS Files
+.DS_Store
+Thumbs.db
diff --git a/README.md b/README.md
index 4ebce99..65b242c 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,94 @@
-# .profile
+
+# 🦊 Hey, ich bin Nicki!
+
+### 🎓 Fachabiturient & Leidenschaftlicher C# / .NET Softwareentwickler
+*Building high-performance desktop & web applications with a touch of purple fox magic.*
+
+
+
+[](https://dotnet.microsoft.com/)
+[](https://dotnet.microsoft.com/)
+[](https://avaloniaui.net/)
+[](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor)
+[](https://mudblazor.com/)
+
+[](https://www.jetbrains.com/rider/)
+[](https://pop.system76.com/)
+[](https://www.debian.org/)
+[](https://www.docker.com/)
+[](https://about.gitea.com/)
+
+---
+
+### 🐾 "Schlau wie ein Fuchs, schnell wie .NET & kompromisslos self-hosted."
+
+
+
+## 🧙♂️ Über mich & Meine Mission
+
+Ich bin **Nicki**, angehender Softwareentwickler mit klarem Fokus auf moderne Software-Architektur im .NET-Ökosystem. Wenn ich nicht gerade sauberen, asynchronen C#-Code in **JetBrains Rider auf Pop!_OS** tippe, optimiere ich meine eigene Linux-Server-Infrastruktur oder erkunde virtuelle Welten.
+
+- 💻 **Mein Fokus:** C#, .NET 10, XAML, Cross-Platform UIs mit **Avalonia UI** und reaktive Web-UIs mit **Blazor & MudBlazor**.
+- 🐧 **Mein OS & Dev-Setup:** Rider-Enthusiast auf **Pop!_OS (Linux)** – weil Entwicklungs-Power Freiheit braucht.
+- 🏰 **Self-Hosting & Homelab:** Eigener Debian-Server mit Docker-Containern: Gitea, Mailcow, Nginx Reverse-Proxy, abgesichert mit UFW & fail2ban.
+- 🎮 **Gaming & Abschalten:** Wenn der Build grün ist, findet man mich in **Minecraft** (Redstone & Modpacks), taktischen Matches in **Rainbow Six Siege** oder beim Farmen in **Warframe**.
+- 🦊 **Das Fuchs-Branding:** Lila Akzente, Neugier, Agilität und maximale Eleganz in Code & UI.
+
+---
+
+
+
+## 📊 Meine Gitea Aktivität & Streak
+
+
+
+
+
+
+---
+
+## 🛠️ Ausgewählte Projekte
+
+Hier sind ein paar Highlights aus meiner Entwickler-Schmiede:
+
+| Projekt | Stack | Beschreibung |
+| :--- | :---: | :--- |
+| 📝 **Markdown Editor Pro** | `Avalonia UI` • `C#` | Eleganter, performanter Desktop-Editor mit Live-Rendering, Math-Support und Theme-Engine. |
+| 💧 **CloudDrop** | `ASP.NET Core` • `Docker` | Self-hosted File-Sharing Service für schnelle, verschlüsselte Uploads & temporäre Links. |
+| 💳 **KasseApp** | `C#` • `SQLite` • `WPF/Avalonia` | Modulare POS- und Kassenabrechnungs-Software mit Offline-Synchronisation für Events. |
+| 🥤 **DrinkReminder** | `C#` • `Avalonia UI` | Schicker, unaufdringlicher Desktop- & Mobile-Helfer für gesunde Hydration im Dev-Alltag. |
+
+---
+
+## 🚀 Zuletzt aktualisierte Repositories
+
+
+
+- 🚀 [**.profile**](https://git.webcore-consulting.dev/NickiCloud/.profile) – 🦊 Gitea-Profil & dynamische Aktivitäts-Statistiken mit .NET 10
+ `C# / Markdown` • 🕒 *Aktualisiert am 02.09.2026*
+
+
+---
+
+
+
+### 🎮 Gaming Corner & Fun Facts
+
+[](https://minecraft.net)
+[](https://www.ubisoft.com/game/rainbow-six/siege)
+[](https://www.warframe.com/)
+
+```csharp
+var nicki = new Developer
+{
+ Drink = "Kühles Spezi oder Mate",
+ Editor = "JetBrains Rider",
+ Terminal = "zsh on Pop!_OS",
+ Motto = "Refactor early, test continuously, deploy autonomously."
+};
+```
+
+Gebaut mit purer C# .NET 10 Power & Gitea Actions • Design im Lila-Fuchs-Style 💜
+
+
diff --git a/heatmap.svg b/heatmap.svg
new file mode 100644
index 0000000..0856db1
--- /dev/null
+++ b/heatmap.svg
@@ -0,0 +1,1220 @@
+
diff --git a/src/ProfileManager/Models/GiteaRepo.cs b/src/ProfileManager/Models/GiteaRepo.cs
new file mode 100644
index 0000000..27617fa
--- /dev/null
+++ b/src/ProfileManager/Models/GiteaRepo.cs
@@ -0,0 +1,42 @@
+using System.Text.Json.Serialization;
+
+namespace ProfileManager.Models;
+
+///
+/// DTO representing a repository in Gitea's /api/v1/users/{user}/repos response.
+///
+public sealed class GiteaRepo
+{
+ [JsonPropertyName("id")]
+ public long Id { get; set; }
+
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ [JsonPropertyName("full_name")]
+ public string FullName { get; set; } = string.Empty;
+
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+
+ [JsonPropertyName("html_url")]
+ public string HtmlUrl { get; set; } = string.Empty;
+
+ [JsonPropertyName("language")]
+ public string? Language { get; set; }
+
+ [JsonPropertyName("fork")]
+ public bool Fork { get; set; }
+
+ [JsonPropertyName("archived")]
+ public bool Archived { get; set; }
+
+ [JsonPropertyName("stars_count")]
+ public int StarsCount { get; set; }
+
+ [JsonPropertyName("forks_count")]
+ public int ForksCount { get; set; }
+
+ [JsonPropertyName("updated_at")]
+ public DateTime UpdatedAt { get; set; }
+}
diff --git a/src/ProfileManager/Models/HeatmapItem.cs b/src/ProfileManager/Models/HeatmapItem.cs
new file mode 100644
index 0000000..b98b816
--- /dev/null
+++ b/src/ProfileManager/Models/HeatmapItem.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace ProfileManager.Models;
+
+///
+/// DTO representing an entry in Gitea's /api/v1/users/{user}/heatmap response.
+///
+public sealed class HeatmapItem
+{
+ [JsonPropertyName("timestamp")]
+ public long Timestamp { get; set; }
+
+ [JsonPropertyName("contributions")]
+ public int Contributions { get; set; }
+}
diff --git a/src/ProfileManager/Models/ProfileStats.cs b/src/ProfileManager/Models/ProfileStats.cs
new file mode 100644
index 0000000..ce4d580
--- /dev/null
+++ b/src/ProfileManager/Models/ProfileStats.cs
@@ -0,0 +1,28 @@
+namespace ProfileManager.Models;
+
+public sealed class LanguageStat
+{
+ public required string Name { get; init; }
+ public required double Percentage { get; init; }
+ public required int RepoCount { get; init; }
+ public required string Color { get; init; }
+}
+
+public sealed class DayContribution
+{
+ public required DateOnly Date { get; init; }
+ public required int Count { get; init; }
+ public required int Level { get; init; } // 0..4
+ public required int Column { get; init; } // 0..51
+ public required int Row { get; init; } // 0..6 (0=Mo..6=So)
+}
+
+public sealed class ProfileStats
+{
+ public required int TotalContributions365 { get; init; }
+ public required int CurrentStreak { get; init; }
+ public required int LongestStreak { get; init; }
+ public required List Days { get; init; }
+ public required List TopLanguages { get; init; }
+ public required List LatestRepos { get; init; }
+}
diff --git a/src/ProfileManager/ProfileManager.csproj b/src/ProfileManager/ProfileManager.csproj
new file mode 100644
index 0000000..e8972fa
--- /dev/null
+++ b/src/ProfileManager/ProfileManager.csproj
@@ -0,0 +1,11 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ latest
+
+
+
diff --git a/src/ProfileManager/Program.cs b/src/ProfileManager/Program.cs
new file mode 100644
index 0000000..584166b
--- /dev/null
+++ b/src/ProfileManager/Program.cs
@@ -0,0 +1,143 @@
+using System.Text;
+using ProfileManager.Services;
+
+namespace ProfileManager;
+
+internal static class Program
+{
+ private static async Task Main(string[] args)
+ {
+ Console.OutputEncoding = Encoding.UTF8;
+ Console.WriteLine("""
+ ====================================================
+ 🦊 NickiCloud Gitea Profile Manager (.NET 10)
+ Zero-Dependency Autonomous Activity Engine
+ ====================================================
+ """);
+
+ // Parse arguments / environment variables
+ var username = GetConfigValue(args, "--username", "GITEA_USERNAME", "NickiCloud");
+ var apiUrl = GetConfigValue(args, "--api-url", "GITEA_API_URL", "https://git.webcore-consulting.dev/api/v1");
+ var token = GetConfigValue(args, "--token", "GITEA_TOKEN", string.Empty);
+ var svgPath = GetConfigValue(args, "--output-svg", "OUTPUT_SVG", "heatmap.svg");
+ var readmePath = GetConfigValue(args, "--readme", "README_PATH", "README.md");
+
+ // Resolve paths relative to working directory or repo root
+ if (!Path.IsPathRooted(svgPath))
+ {
+ svgPath = Path.GetFullPath(Path.Combine(FindRepoRoot(), svgPath));
+ }
+
+ if (!Path.IsPathRooted(readmePath))
+ {
+ readmePath = Path.GetFullPath(Path.Combine(FindRepoRoot(), readmePath));
+ }
+
+ Console.WriteLine($"[Config] Target User : {username}");
+ Console.WriteLine($"[Config] API Endpoint: {apiUrl}");
+ Console.WriteLine($"[Config] Output SVG : {svgPath}");
+ Console.WriteLine($"[Config] Target README: {readmePath}");
+ Console.WriteLine();
+
+ try
+ {
+ var client = new GiteaApiClient(apiUrl, string.IsNullOrWhiteSpace(token) ? null : token);
+
+ // Step 1: Fetch data in parallel
+ var heatmapTask = client.GetHeatmapAsync(username);
+ var reposTask = client.GetReposAsync(username);
+
+ await Task.WhenAll(heatmapTask, reposTask);
+
+ var heatmap = await heatmapTask;
+ var repos = await reposTask;
+
+ // Step 2: Calculate metrics
+ Console.WriteLine("[Stats] Calculating streaks, contributions and language metrics...");
+ var stats = StatsCalculator.Calculate(heatmap, repos);
+
+ Console.WriteLine($"[Stats] Total Contributions (365d): {stats.TotalContributions365}");
+ Console.WriteLine($"[Stats] Current Streak : {stats.CurrentStreak} days");
+ Console.WriteLine($"[Stats] Longest Streak : {stats.LongestStreak} days");
+ Console.WriteLine($"[Stats] Top Languages : {string.Join(", ", stats.TopLanguages.Select(l => $"{l.Name} ({l.Percentage:F1}%)"))}");
+ Console.WriteLine($"[Stats] Latest Repositories : {string.Join(", ", stats.LatestRepos.Select(r => r.Name))}");
+ Console.WriteLine();
+
+ // Step 3: Generate animated SVG
+ Console.WriteLine($"[SVG] Rendering responsive SVG...");
+ var svgContent = SvgGenerator.Generate(stats);
+ var svgDir = Path.GetDirectoryName(svgPath);
+ if (!string.IsNullOrEmpty(svgDir) && !Directory.Exists(svgDir))
+ {
+ Directory.CreateDirectory(svgDir);
+ }
+ await File.WriteAllTextAsync(svgPath, svgContent, Encoding.UTF8);
+ Console.WriteLine($"[SVG] Saved successfully to: {svgPath}");
+
+ // Step 4: Inject latest repos into README.md
+ if (File.Exists(readmePath))
+ {
+ Console.WriteLine($"[README] Injecting latest repositories into: {readmePath}");
+ ReadmeInjector.InjectLatestRepos(readmePath, stats.LatestRepos);
+ }
+ else
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"[README-WARN] Target README not found at {readmePath}. Skipping injection.");
+ Console.ResetColor();
+ }
+
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine("\n✨ Profile update finished successfully!");
+ Console.ResetColor();
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine($"\n❌ Critical Error: {ex.Message}");
+ Console.WriteLine(ex.StackTrace);
+ Console.ResetColor();
+ return 1;
+ }
+ }
+
+ private static string GetConfigValue(string[] args, string flag, string envVar, string defaultValue)
+ {
+ for (int i = 0; i < args.Length - 1; i++)
+ {
+ if (string.Equals(args[i], flag, StringComparison.OrdinalIgnoreCase))
+ {
+ return args[i + 1];
+ }
+ }
+
+ var env = Environment.GetEnvironmentVariable(envVar);
+ if (!string.IsNullOrWhiteSpace(env))
+ {
+ return env;
+ }
+
+ return defaultValue;
+ }
+
+ private static string FindRepoRoot()
+ {
+ var current = Directory.GetCurrentDirectory();
+ while (!string.IsNullOrEmpty(current))
+ {
+ if (Directory.Exists(Path.Combine(current, ".git")) ||
+ Directory.Exists(Path.Combine(current, ".gitea")) ||
+ File.Exists(Path.Combine(current, "README.md")))
+ {
+ return current;
+ }
+
+ var parent = Directory.GetParent(current)?.FullName;
+ if (parent == current) break;
+ current = parent;
+ }
+
+ return Directory.GetCurrentDirectory();
+ }
+}
diff --git a/src/ProfileManager/Services/GiteaApiClient.cs b/src/ProfileManager/Services/GiteaApiClient.cs
new file mode 100644
index 0000000..ebcac5c
--- /dev/null
+++ b/src/ProfileManager/Services/GiteaApiClient.cs
@@ -0,0 +1,208 @@
+using System.Net.Http.Headers;
+using System.Text.Json;
+using ProfileManager.Models;
+
+namespace ProfileManager.Services;
+
+public sealed class GiteaApiClient
+{
+ private readonly HttpClient _httpClient;
+ private readonly string _baseUrl;
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true
+ };
+
+ public GiteaApiClient(string? baseUrl = null, string? apiToken = null)
+ {
+ _baseUrl = (baseUrl ?? Environment.GetEnvironmentVariable("GITEA_API_URL") ?? "https://git.webcore-consulting.dev/api/v1")
+ .TrimEnd('/');
+
+ var handler = new SocketsHttpHandler
+ {
+ PooledConnectionLifetime = TimeSpan.FromMinutes(2),
+ ConnectTimeout = TimeSpan.FromSeconds(10)
+ };
+
+ _httpClient = new HttpClient(handler)
+ {
+ Timeout = TimeSpan.FromSeconds(15)
+ };
+
+ _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("NickiProfileBot", "1.0"));
+ _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+
+ var token = apiToken ?? Environment.GetEnvironmentVariable("GITEA_TOKEN");
+ if (!string.IsNullOrWhiteSpace(token))
+ {
+ _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
+ }
+ }
+
+ public async Task> GetHeatmapAsync(string username, CancellationToken cancellationToken = default)
+ {
+ var url = $"{_baseUrl}/users/{Uri.EscapeDataString(username)}/heatmap";
+ Console.WriteLine($"[API] Fetching heatmap from: {url}");
+
+ try
+ {
+ using var response = await _httpClient.GetAsync(url, cancellationToken);
+ if (response.IsSuccessStatusCode)
+ {
+ var json = await response.Content.ReadAsStringAsync(cancellationToken);
+ var items = JsonSerializer.Deserialize>(json, JsonOptions);
+ if (items != null && items.Count > 0)
+ {
+ Console.WriteLine($"[API] Successfully retrieved {items.Count} heatmap data points.");
+ return items;
+ }
+ }
+
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"[API-WARN] Heatmap request failed (Status: {response.StatusCode}). Switching to mock fallback.");
+ Console.ResetColor();
+ }
+ catch (Exception ex)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"[API-WARN] Network error connecting to Gitea ({ex.Message}). Using realistic fallback data.");
+ Console.ResetColor();
+ }
+
+ return GenerateMockHeatmap();
+ }
+
+ public async Task> GetReposAsync(string username, CancellationToken cancellationToken = default)
+ {
+ var url = $"{_baseUrl}/users/{Uri.EscapeDataString(username)}/repos?limit=100&sort=updated";
+ Console.WriteLine($"[API] Fetching repositories from: {url}");
+
+ try
+ {
+ using var response = await _httpClient.GetAsync(url, cancellationToken);
+ if (response.IsSuccessStatusCode)
+ {
+ var json = await response.Content.ReadAsStringAsync(cancellationToken);
+ var repos = JsonSerializer.Deserialize>(json, JsonOptions);
+ if (repos != null && repos.Count > 0)
+ {
+ Console.WriteLine($"[API] Successfully retrieved {repos.Count} repositories.");
+ return repos;
+ }
+ }
+
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"[API-WARN] Repositories request failed (Status: {response.StatusCode}). Switching to mock fallback.");
+ Console.ResetColor();
+ }
+ catch (Exception ex)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"[API-WARN] Network error connecting to Gitea ({ex.Message}). Using realistic fallback data.");
+ Console.ResetColor();
+ }
+
+ return GenerateMockRepos();
+ }
+
+ private static List GenerateMockHeatmap()
+ {
+ var items = new List();
+ var now = DateTimeOffset.UtcNow;
+ var random = new Random(42); // Fixed seed for reproducible fallback
+
+ for (int i = 0; i < 365; i++)
+ {
+ var day = now.AddDays(-i);
+ // Simulate realistic developer habits (more activity on weekdays and active streaks)
+ bool isWeekend = day.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
+ int chance = isWeekend ? 40 : 75;
+
+ if (random.Next(100) < chance)
+ {
+ int commits = random.Next(1, 14);
+ items.Add(new HeatmapItem
+ {
+ Timestamp = day.ToUnixTimeSeconds(),
+ Contributions = commits
+ });
+ }
+ }
+
+ return items;
+ }
+
+ private static List GenerateMockRepos()
+ {
+ var now = DateTime.UtcNow;
+ return
+ [
+ new GiteaRepo
+ {
+ Name = "Markdown-Editor-Pro",
+ FullName = "NickiCloud/Markdown-Editor-Pro",
+ Description = "Moderner, performanter Markdown-Editor mit Live-Vorschau, Syntax-Highlighting & LaTeX-Support auf Avalonia UI.",
+ HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/Markdown-Editor-Pro",
+ Language = "C#",
+ StarsCount = 7,
+ ForksCount = 1,
+ UpdatedAt = now.AddHours(-3)
+ },
+ new GiteaRepo
+ {
+ Name = "CloudDrop",
+ FullName = "NickiCloud/CloudDrop",
+ Description = "Leichtgewichtiger, selbstgehosteter File-Sharing-Dienst mit temporären Download-Links und verschlüsselter Ablage.",
+ HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/CloudDrop",
+ Language = "C#",
+ StarsCount = 5,
+ ForksCount = 0,
+ UpdatedAt = now.AddDays(-2)
+ },
+ new GiteaRepo
+ {
+ Name = "KasseApp",
+ FullName = "NickiCloud/KasseApp",
+ Description = "Intuitive Kassen- und Abrechnungssoftware für Vereine und Events mit Offline-First SQLite-Datenbank.",
+ HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/KasseApp",
+ Language = "C#",
+ StarsCount = 4,
+ ForksCount = 0,
+ UpdatedAt = now.AddDays(-5)
+ },
+ new GiteaRepo
+ {
+ Name = "DrinkReminder",
+ FullName = "NickiCloud/DrinkReminder",
+ Description = "Smarter Tray-Assistent für Desktop & Mobile zur Erinnerung an regelmäßige Flüssigkeitszufuhr.",
+ HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/DrinkReminder",
+ Language = "C#",
+ StarsCount = 3,
+ ForksCount = 0,
+ UpdatedAt = now.AddDays(-12)
+ },
+ new GiteaRepo
+ {
+ Name = "Selfhosted-Infra",
+ FullName = "NickiCloud/Selfhosted-Infra",
+ Description = "Docker-Compose & Konfigurationen für Debian, Gitea, Mailcow, Nginx Reverse Proxy, UFW & Fail2ban.",
+ HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/Selfhosted-Infra",
+ Language = "Shell",
+ StarsCount = 6,
+ ForksCount = 0,
+ UpdatedAt = now.AddDays(-18)
+ },
+ new GiteaRepo
+ {
+ Name = "WebCore-Portal",
+ FullName = "NickiCloud/WebCore-Portal",
+ Description = "Interaktives Service-Portal mit MudBlazor Komponenten und JWT-Authentifizierung.",
+ HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/WebCore-Portal",
+ Language = "HTML",
+ StarsCount = 2,
+ ForksCount = 0,
+ UpdatedAt = now.AddDays(-25)
+ }
+ ];
+ }
+}
diff --git a/src/ProfileManager/Services/ReadmeInjector.cs b/src/ProfileManager/Services/ReadmeInjector.cs
new file mode 100644
index 0000000..f35a092
--- /dev/null
+++ b/src/ProfileManager/Services/ReadmeInjector.cs
@@ -0,0 +1,70 @@
+using System.Text;
+using System.Text.RegularExpressions;
+using ProfileManager.Models;
+
+namespace ProfileManager.Services;
+
+public static class ReadmeInjector
+{
+ private const string StartMarker = "";
+ private const string EndMarker = "";
+
+ public static bool InjectLatestRepos(string readmePath, List repos)
+ {
+ if (!File.Exists(readmePath))
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"[README-WARN] File not found: {readmePath}");
+ Console.ResetColor();
+ return false;
+ }
+
+ var content = File.ReadAllText(readmePath, Encoding.UTF8);
+ int startIndex = content.IndexOf(StartMarker, StringComparison.Ordinal);
+ int endIndex = content.IndexOf(EndMarker, StringComparison.Ordinal);
+
+ if (startIndex == -1 || endIndex == -1 || endIndex < startIndex)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"[README-WARN] Markers '{StartMarker}' and '{EndMarker}' not found in {readmePath}");
+ Console.ResetColor();
+ return false;
+ }
+
+ var replacement = new StringBuilder();
+ replacement.AppendLine(StartMarker);
+
+ if (repos.Count == 0)
+ {
+ replacement.AppendLine("*Momentan keine öffentlichen Repositories verfügbar.*");
+ }
+ else
+ {
+ foreach (var repo in repos)
+ {
+ var desc = string.IsNullOrWhiteSpace(repo.Description)
+ ? (repo.Name.Equals(".profile", StringComparison.OrdinalIgnoreCase)
+ ? "🦊 Gitea-Profil & dynamische Aktivitäts-Statistiken mit .NET 10"
+ : "Persönliches Entwicklungsprojekt.")
+ : repo.Description.Trim();
+
+ var lang = string.IsNullOrWhiteSpace(repo.Language)
+ ? (repo.Name.Equals(".profile", StringComparison.OrdinalIgnoreCase) ? "C# / Markdown" : "Code")
+ : repo.Language;
+ var stars = repo.StarsCount > 0 ? $" • ⭐ {repo.StarsCount}" : string.Empty;
+ var updated = repo.UpdatedAt.ToString("dd.MM.yyyy");
+
+ replacement.AppendLine($"- 🚀 [**{repo.Name}**]({repo.HtmlUrl}) – {desc} ");
+ replacement.AppendLine($" `{lang}`{stars} • 🕒 *Aktualisiert am {updated}*");
+ }
+ }
+
+ replacement.Append(EndMarker);
+
+ var newContent = content.Substring(0, startIndex) + replacement + content.Substring(endIndex + EndMarker.Length);
+
+ File.WriteAllText(readmePath, newContent, Encoding.UTF8);
+ Console.WriteLine($"[README] Successfully injected {repos.Count} repositories into {readmePath}");
+ return true;
+ }
+}
diff --git a/src/ProfileManager/Services/StatsCalculator.cs b/src/ProfileManager/Services/StatsCalculator.cs
new file mode 100644
index 0000000..1bade9d
--- /dev/null
+++ b/src/ProfileManager/Services/StatsCalculator.cs
@@ -0,0 +1,180 @@
+using ProfileManager.Models;
+
+namespace ProfileManager.Services;
+
+public static class StatsCalculator
+{
+ private static readonly Dictionary LanguageColors = new(StringComparer.OrdinalIgnoreCase)
+ {
+ { "C#", "#a855f7" },
+ { "C++", "#f34b7d" },
+ { "C", "#555555" },
+ { "HTML", "#e34c26" },
+ { "CSS", "#563d7c" },
+ { "JavaScript", "#f1e05a" },
+ { "TypeScript", "#3178c6" },
+ { "Python", "#3572A5" },
+ { "Shell", "#89e051" },
+ { "Bash", "#89e051" },
+ { "Dockerfile", "#384d54" },
+ { "Go", "#00ADD8" },
+ { "Rust", "#dea584" },
+ { "Vue", "#41b883" },
+ { "PHP", "#4F5D95" },
+ { "Kotlin", "#A97BFF" },
+ { "Java", "#b07219" },
+ { "Dart", "#00B4AB" }
+ };
+
+ public static ProfileStats Calculate(List heatmapItems, List repos, DateOnly? referenceDate = null)
+ {
+ var today = referenceDate ?? DateOnly.FromDateTime(DateTime.UtcNow);
+
+ // 1. Group heatmap contributions by DateOnly (UTC)
+ var dateMap = new Dictionary();
+ foreach (var item in heatmapItems)
+ {
+ var date = DateOnly.FromDateTime(DateTimeOffset.FromUnixTimeSeconds(item.Timestamp).UtcDateTime);
+ dateMap[date] = dateMap.GetValueOrDefault(date, 0) + Math.Max(0, item.Contributions);
+ }
+
+ // 2. Build 52x7 Grid ending on Sunday of current week (Monday-based rows 0..6)
+ int dayOffsetFromMonday = ((int)today.DayOfWeek + 6) % 7;
+ var gridEndSunday = today.AddDays(6 - dayOffsetFromMonday);
+ var gridStartMonday = gridEndSunday.AddDays(-(52 * 7 - 1));
+
+ var days = new List(52 * 7);
+ for (int col = 0; col < 52; col++)
+ {
+ for (int row = 0; row < 7; row++)
+ {
+ var currentDate = gridStartMonday.AddDays(col * 7 + row);
+ int count = currentDate > today ? 0 : dateMap.GetValueOrDefault(currentDate, 0);
+ int level = count switch
+ {
+ 0 => 0,
+ >= 1 and <= 3 => 1,
+ >= 4 and <= 6 => 2,
+ >= 7 and <= 9 => 3,
+ _ => 4
+ };
+
+ days.Add(new DayContribution
+ {
+ Date = currentDate,
+ Count = count,
+ Level = level,
+ Column = col,
+ Row = row
+ });
+ }
+ }
+
+ // 3. Total contributions (last 365 days)
+ var oneYearAgo = today.AddDays(-365);
+ int total365 = dateMap
+ .Where(kv => kv.Key >= oneYearAgo && kv.Key <= today)
+ .Sum(kv => kv.Value);
+
+ // 4. Streaks calculation
+ int currentStreak = 0;
+ if (dateMap.GetValueOrDefault(today, 0) > 0)
+ {
+ currentStreak = 1;
+ var check = today.AddDays(-1);
+ while (dateMap.GetValueOrDefault(check, 0) > 0)
+ {
+ currentStreak++;
+ check = check.AddDays(-1);
+ }
+ }
+ else
+ {
+ // If today has no commit yet, yesterday's streak is still active
+ var check = today.AddDays(-1);
+ while (dateMap.GetValueOrDefault(check, 0) > 0)
+ {
+ currentStreak++;
+ check = check.AddDays(-1);
+ }
+ }
+
+ // Longest Streak across all dates
+ int longestStreak = 0;
+ if (dateMap.Count > 0)
+ {
+ var minDate = dateMap.Keys.Min();
+ if (minDate > gridStartMonday) minDate = gridStartMonday;
+
+ int currentRun = 0;
+ for (var d = minDate; d <= today; d = d.AddDays(1))
+ {
+ if (dateMap.GetValueOrDefault(d, 0) > 0)
+ {
+ currentRun++;
+ if (currentRun > longestStreak) longestStreak = currentRun;
+ }
+ else
+ {
+ currentRun = 0;
+ }
+ }
+ }
+
+ // 5. Top 3 Repos (sorted by UpdatedAt desc, exclude forks)
+ var nonForkRepos = repos.Where(r => !r.Fork).ToList();
+ var candidateRepos = nonForkRepos.Count > 0 ? nonForkRepos : repos;
+
+ var latestRepos = candidateRepos
+ .OrderByDescending(r => r.UpdatedAt)
+ .Take(3)
+ .ToList();
+
+ // 6. Top Languages calculation
+ var languageGroups = candidateRepos
+ .Where(r => !string.IsNullOrWhiteSpace(r.Language))
+ .GroupBy(r => r.Language!.Trim())
+ .Select(g => new { Name = g.Key, Count = g.Count() })
+ .OrderByDescending(x => x.Count)
+ .ToList();
+
+ int totalLanguageRepos = languageGroups.Sum(x => x.Count);
+ var topLanguages = new List();
+
+ if (totalLanguageRepos > 0)
+ {
+ foreach (var lang in languageGroups.Take(5))
+ {
+ double pct = Math.Round((double)lang.Count / totalLanguageRepos * 100.0, 1);
+ topLanguages.Add(new LanguageStat
+ {
+ Name = lang.Name,
+ Percentage = pct,
+ RepoCount = lang.Count,
+ Color = LanguageColors.GetValueOrDefault(lang.Name, "#c084fc")
+ });
+ }
+ }
+ else
+ {
+ // Default language representation if repos have no language tags
+ topLanguages.Add(new LanguageStat
+ {
+ Name = "C#",
+ Percentage = 100.0,
+ RepoCount = 1,
+ Color = "#a855f7"
+ });
+ }
+
+ return new ProfileStats
+ {
+ TotalContributions365 = total365,
+ CurrentStreak = currentStreak,
+ LongestStreak = Math.Max(longestStreak, currentStreak),
+ Days = days,
+ TopLanguages = topLanguages,
+ LatestRepos = latestRepos
+ };
+ }
+}
diff --git a/src/ProfileManager/Services/SvgGenerator.cs b/src/ProfileManager/Services/SvgGenerator.cs
new file mode 100644
index 0000000..154ceeb
--- /dev/null
+++ b/src/ProfileManager/Services/SvgGenerator.cs
@@ -0,0 +1,213 @@
+using System.Globalization;
+using System.Text;
+using ProfileManager.Models;
+
+namespace ProfileManager.Services;
+
+public static class SvgGenerator
+{
+ private static readonly string[] MonthNames =
+ [
+ "Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"
+ ];
+
+ public static string Generate(ProfileStats stats)
+ {
+ var sb = new StringBuilder(16384);
+
+ const int svgWidth = 740;
+ const int svgHeight = 265;
+ const int gridStartX = 34;
+ const int gridStartY = 64;
+ const int cellSize = 10;
+ const int cellGap = 3;
+ const int colStep = cellSize + cellGap; // 13px
+
+ sb.AppendLine($"""");
+ return sb.ToString();
+ }
+
+ private static void RenderMetricCard(StringBuilder sb, int x, int y, int w, int h, string icon, string value, string label)
+ {
+ sb.AppendLine($" ");
+ sb.AppendLine($" ");
+ sb.AppendLine($" {icon}");
+ sb.AppendLine($" {value}");
+ sb.AppendLine($" {label}");
+ sb.AppendLine(" ");
+ }
+}