diff --git a/.gitea/workflows/update-profile.yml b/.gitea/workflows/update-profile.yml
index 85c8f4d..f927d9a 100644
--- a/.gitea/workflows/update-profile.yml
+++ b/.gitea/workflows/update-profile.yml
@@ -1,4 +1,4 @@
-name: Update Gitea Profile
+name: Update Gitea Profile & Team Telemetry
on:
schedule:
@@ -12,6 +12,7 @@ on:
- main
paths-ignore:
- 'heatmap.svg'
+ - 'team-stats.svg'
- 'README.md'
jobs:
@@ -27,7 +28,6 @@ jobs:
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
@@ -49,17 +49,17 @@ jobs:
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
+ # Prüfen, ob sich SVG-Dateien oder README.md geändert haben
+ if git status --porcelain | grep -E "(heatmap.svg|team-stats.svg|README.md)"; then
+ echo "Änderungen an Grafiken oder README.md erkannt. Committe..."
+ git add heatmap.svg team-stats.svg README.md
# GANZ WICHTIG: [skip ci] verhindert Endlosschleifen in Gitea Actions!
- git commit -m "chore(profile): update activity heatmap & stats [skip ci]"
+ git commit -m "chore(profile): update personal & team telemetry [skip ci]"
# Änderungen pushen
git push
- echo "Profil erfolgreich aktualisiert und gepusht! ✨"
+ echo "Profil & Team-Telemetrie erfolgreich aktualisiert und gepusht! ✨"
else
echo "Keine Änderungen vorhanden. Alles auf dem neuesten Stand. 👍"
fi
diff --git a/README.md b/README.md
index 521502e..87805b7 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
# 🦊 Hey, ich bin Nicki!
-### 🎓 Fachabiturient & Leidenschaftlicher C# / .NET Softwareentwickler
-*Building high-performance desktop & web applications with a touch of purple fox magic.*
+### 🎓 Fachabiturient & Passionierter C# / .NET Software-Architekt
+*High-Performance Desktop Apps, moderne Web-Services & kompromissloses Self-Hosting im lila Fuchs-Style.*
@@ -17,63 +17,130 @@
[](https://pop.system76.com/)
[](https://www.debian.org/)
[](https://www.docker.com/)
-[](https://about.gitea.com/)
+[](https://www.webcore-consulting.de)
---
-### 🐾 "Schlau wie ein Fuchs, schnell wie .NET & kompromisslos self-hosted."
+### 🐾 "Schlau wie ein Fuchs, skalierbar wie .NET 10 & kompromisslos self-hosted."
-## 🧙♂️ Über mich & Meine Mission
+## 🧙♂️ Über mich, Vision & Tech-Stack
-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.
+Ich bin **Nicki (Nicklas)**, angehender Softwareentwickler mit fundiertem Fokus auf performante Softwareentwicklung im **C# / .NET 10** Ökosystem. Neben der Anwendungsentwicklung schlägt mein Herz für Linux-Systemadministration, Containerisierung und sichere Netzwerk-Infrastrukturen.
-- 💻 **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.
+- 💻 **Fokus:** C#, .NET 10, Cross-Platform Desktop mit **Avalonia UI**, moderne Web-Apps mit **Blazor / MudBlazor** und modulare Microservices.
+- 🏢 **Team & Consulting:** Softwareentwickler bei **WebCore Consulting** – Entwicklung von Management-Tools, Kundenportalen und Security-Services.
+- 🐧 **Entwicklungsumgebung:** Vollständiger Linux-Workflow mit **JetBrains Rider auf Pop!_OS** – maximale Produktivität ohne Windows-Overhead.
+- 🏰 **Self-Hosted Homelab:** Eigener Debian-Rootserver mit Docker-Containern: Gitea & Gitea Actions, Mailcow Enterprise Suite, Nginx Reverse Proxy mit Let's Encrypt, gehärtet durch UFW & fail2ban.
+- 🎮 **Gaming Corner:** Wenn der Build grün ist: Redstone-Komplexe in **Minecraft**, taktische Runden in **Rainbow Six Siege** oder Grinden in **Warframe**.
+- 🦊 **Branding:** Lila Fuchs (Purple Fox) – steht für Agilität, Eleganz, Scharfsinn und sauberen Code.
---
-## 📊 Meine Gitea Aktivität & Streak
-
-
-
+## 📊 Live Aktivitäts- & Performance-Dashboards
----
+
+🦊 Tab 1: Persönliche Aktivität & Heatmap
-## 🛠️ Ausgewählte Projekte
+> *Vollautomatisch generiert über Gitea Actions & .NET 10 – trackt Commits, Streaks und Jahresbeiträge.*
-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
-
-
+#### 📌 Zuletzt aktualisierte persönliche Projekte:
- 🚀 [**.profile**](https://git.webcore-consulting.dev/NickiCloud/.profile) – 🦊 Gitea-Profil & dynamische Aktivitäts-Statistiken mit .NET 10
`C#` • 🕒 *Aktualisiert am 02.09.2026*
+
+
+
+
+
+🏢 Tab 2: Team- & Organisations-Aktivität (WebCore Consulting)
+
+> *Live-Telemetrie aller Team-Repositories, Code-Volumen und Byte-genaue Sprachverteilung.*
+
+
+
+
+
+#### 📈 Byte-genaue Sprachverteilung (Team-Codebase):
+
+| Sprache | Anteil | Code-Volumen | Verteilung |
+| :--- | :---: | :---: | :--- |
+| `C#` | **56,2%** | 1,1 MB | `███████████░░░░░░░░░` |
+| `HTML` | **40,4%** | 797,4 KB | `████████░░░░░░░░░░░░` |
+| `CSS` | **2,3%** | 45,7 KB | `░░░░░░░░░░░░░░░░░░░░` |
+| `JavaScript` | **0,4%** | 8,6 KB | `░░░░░░░░░░░░░░░░░░░░` |
+| `PowerShell` | **0,4%** | 7,6 KB | `░░░░░░░░░░░░░░░░░░░░` |
+| `Shell` | **0,2%** | 3,4 KB | `░░░░░░░░░░░░░░░░░░░░` |
+
+
+#### 🚀 Aktive Team-Repositories:
+
+- 🏢 [**WebCore/ServerManagement**](https://git.webcore-consulting.dev/WebCore/ServerManagement) 🔒 – Internes Verwaltungs management System
+ `HTML` • 🕒 *Zuletzt bearbeitet: 02.09.2026*
+- 🏢 [**WebCore/USBSafe**](https://git.webcore-consulting.dev/WebCore/USBSafe) 🔒 – WebCore Consulting Projekt
+ `C#` • 🕒 *Zuletzt bearbeitet: 02.09.2026*
+- 🏢 [**WebCore/ServerHub-redesign**](https://git.webcore-consulting.dev/WebCore/ServerHub-redesign) – WebCore Consulting Projekt
+ `C# / Web` • 🕒 *Zuletzt bearbeitet: 30.08.2026*
+
+
+
+
+---
+
+## 🛠️ Ausgewählte Eigenprojekte
+
+| Projekt | Stack | Beschreibung |
+| :--- | :---: | :--- |
+| 📝 **Markdown Editor Pro** | `Avalonia UI` • `C#` | Desktop-Editor mit Live-Rendering, Math-Support und Theme-Engine. |
+| 💧 **CloudDrop** | `ASP.NET Core` • `Docker` | Self-hosted File-Sharing Service für schnelle Uploads & temporäre Links. |
+| 💳 **KasseApp** | `C#` • `SQLite` • `Avalonia` | Modulares Kassensystem mit Offline-Synchronisation für Events & Vereine. |
+| 🥤 **DrinkReminder** | `C#` • `Avalonia UI` | Schicker Desktop- & Mobile-Helfer für gesunde Hydration im Entwickleralltag. |
+
+---
+
+## 🛡️ Homelab & Server-Infrastruktur
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ 🌐 WebCore & HomeLab Architecture │
+│ │
+│ [ Internet / Cloudflare ] │
+│ │ │
+│ ▼ │
+│ [ Debian 12 Host ] ─── [ UFW & fail2ban Security Wall ] │
+│ │ │
+│ ▼ │
+│ [ Nginx Reverse Proxy (SSL/TLS Termination) ] │
+│ │ │
+│ ┌───────┼─────────────────────────┬──────────────┐ │
+│ ▼ ▼ ▼ ▼ │
+│ [Gitea] [Act-Runner] [Mailcow] [Services] │
+│ (Git) (C# / .NET 10 Actions) (E-Mail) (WebCore) │
+└─────────────────────────────────────────────────────────────┘
+```
+
+- **Betriebssystem:** Debian GNU/Linux 12 (Bookworm)
+- **Container-Runtime:** Docker Engine & Docker Compose
+- **Reverse Proxy:** Nginx mit automatischen Let's Encrypt TLS-Zertifikaten
+- **Security:** UFW Firewall, fail2ban (SSH & HTTP Brute-Force Schutz)
+- **Monitoring & CI/CD:** Gitea Actions mit dediziertem Docker `act_runner`
+
---
-### 🎮 Gaming Corner & Fun Facts
+### 🎮 Gaming & Fun Corner
[](https://minecraft.net)
[](https://www.ubisoft.com/game/rainbow-six/siege)
@@ -82,13 +149,14 @@ Hier sind ein paar Highlights aus meiner Entwickler-Schmiede:
```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."
+ Drink = "Kühles Spezi oder Club-Mate",
+ Workstation = "Pop!_OS on Linux Kernel",
+ IDE = "JetBrains Rider 2024+",
+ Team = "WebCore Consulting",
+ Motto = "Measure twice, refactor once, ship autonomously."
};
```
-Gebaut mit purer C# .NET 10 Power & Gitea Actions • Design im Lila-Fuchs-Style 💜
+Entwickelt mit C# .NET 10 & Gitea Actions • Design im Lila-Fuchs-Style 💜
diff --git a/heatmap.svg b/heatmap.svg
index a9ce4df..d30fe62 100644
--- a/heatmap.svg
+++ b/heatmap.svg
@@ -71,10 +71,10 @@
- 🦊 NickiCloud • Gitea Activity
+ 🦊 NickiCloud • Persönliche Aktivität
- 23 Commits im letzten Jahr
+ 24 Commits im letzten Jahr
Sep
@@ -1173,7 +1173,7 @@
01.09.2026: 9 Commits
- 02.09.2026: 7 Commits
+ 02.09.2026: 8 Commits
03.09.2026: 0 Commits
@@ -1191,31 +1191,51 @@
- 📦
- 23
- Jahres-Beiträge
+ 📦
+ 24
+ Jahres-Beiträge
- 🔥
- 4 Tage
- Aktueller Streak
+ 🔥
+ 4 Tage
+ Aktueller Streak
- ⚡
- 4 Tage
- Längster Streak
+ ⚡
+ 4 Tage
+ Längster Streak
-
- C#: 100,0%
+
+ C#: 57,0%
+
+
+ HTML: 39,7%
+
+
+ CSS: 2,3%
+
+
+ JavaScript: 0,4%
+
+
+ PowerShell: 0,4%
- C# 100,0%
+ C# 57,0%
+
+ HTML 39,7%
+
+ CSS 2,3%
+
+ JavaScript 0,4%
+
+ PowerShell 0,4%
diff --git a/src/ProfileManager/Models/GiteaRepo.cs b/src/ProfileManager/Models/GiteaRepo.cs
index 27617fa..232e7b9 100644
--- a/src/ProfileManager/Models/GiteaRepo.cs
+++ b/src/ProfileManager/Models/GiteaRepo.cs
@@ -2,14 +2,32 @@ using System.Text.Json.Serialization;
namespace ProfileManager.Models;
+public sealed class RepoOwner
+{
+ [JsonPropertyName("id")]
+ public long Id { get; set; }
+
+ [JsonPropertyName("login")]
+ public string Login { get; set; } = string.Empty;
+
+ [JsonPropertyName("full_name")]
+ public string FullName { get; set; } = string.Empty;
+
+ [JsonPropertyName("avatar_url")]
+ public string AvatarUrl { get; set; } = string.Empty;
+}
+
///
-/// DTO representing a repository in Gitea's /api/v1/users/{user}/repos response.
+/// DTO representing a repository in Gitea's /api/v1/repos responses.
///
public sealed class GiteaRepo
{
[JsonPropertyName("id")]
public long Id { get; set; }
+ [JsonPropertyName("owner")]
+ public RepoOwner? Owner { get; set; }
+
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
@@ -28,15 +46,30 @@ public sealed class GiteaRepo
[JsonPropertyName("fork")]
public bool Fork { get; set; }
+ [JsonPropertyName("private")]
+ public bool Private { get; set; }
+
[JsonPropertyName("archived")]
public bool Archived { get; set; }
+ [JsonPropertyName("size")]
+ public long Size { get; set; }
+
[JsonPropertyName("stars_count")]
public int StarsCount { get; set; }
[JsonPropertyName("forks_count")]
public int ForksCount { get; set; }
+ [JsonPropertyName("open_issues_count")]
+ public int OpenIssuesCount { get; set; }
+
[JsonPropertyName("updated_at")]
public DateTime UpdatedAt { get; set; }
+
+ [JsonIgnore]
+ public Dictionary DetailedLanguages { get; set; } = [];
+
+ public bool IsOrganizationRepo(string personalUsername) =>
+ Owner != null && !string.Equals(Owner.Login, personalUsername, StringComparison.OrdinalIgnoreCase);
}
diff --git a/src/ProfileManager/Models/Organization.cs b/src/ProfileManager/Models/Organization.cs
new file mode 100644
index 0000000..a465a1a
--- /dev/null
+++ b/src/ProfileManager/Models/Organization.cs
@@ -0,0 +1,30 @@
+using System.Text.Json.Serialization;
+
+namespace ProfileManager.Models;
+
+public sealed class GiteaOrganization
+{
+ [JsonPropertyName("id")]
+ public long Id { get; set; }
+
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ [JsonPropertyName("username")]
+ public string Username { get; set; } = string.Empty;
+
+ [JsonPropertyName("full_name")]
+ public string FullName { get; set; } = string.Empty;
+
+ [JsonPropertyName("description")]
+ public string Description { get; set; } = string.Empty;
+
+ [JsonPropertyName("website")]
+ public string Website { get; set; } = string.Empty;
+
+ [JsonPropertyName("location")]
+ public string Location { get; set; } = string.Empty;
+
+ [JsonPropertyName("avatar_url")]
+ public string AvatarUrl { get; set; } = string.Empty;
+}
diff --git a/src/ProfileManager/Models/ProfileStats.cs b/src/ProfileManager/Models/ProfileStats.cs
index ce4d580..a70009d 100644
--- a/src/ProfileManager/Models/ProfileStats.cs
+++ b/src/ProfileManager/Models/ProfileStats.cs
@@ -4,8 +4,17 @@ public sealed class LanguageStat
{
public required string Name { get; init; }
public required double Percentage { get; init; }
+ public required long Bytes { get; init; }
public required int RepoCount { get; init; }
public required string Color { get; init; }
+
+ public string FormattedSize => Bytes switch
+ {
+ >= 1024 * 1024 => $"{(double)Bytes / (1024 * 1024):F1} MB",
+ >= 1024 => $"{(double)Bytes / 1024:F1} KB",
+ > 0 => $"{Bytes} B",
+ _ => "0 B"
+ };
}
public sealed class DayContribution
@@ -17,12 +26,31 @@ public sealed class DayContribution
public required int Row { get; init; } // 0..6 (0=Mo..6=So)
}
+public sealed class TeamStats
+{
+ public required string OrganizationName { get; init; }
+ public required string OrganizationLogin { get; init; }
+ public required string Website { get; init; }
+ public required int TotalRepos { get; init; }
+ public required long TotalCodeBytes { get; init; }
+ public required List Languages { get; init; }
+ public required List Repos { get; init; }
+
+ public string FormattedTotalSize => TotalCodeBytes switch
+ {
+ >= 1024 * 1024 => $"{(double)TotalCodeBytes / (1024 * 1024):F1} MB",
+ >= 1024 => $"{(double)TotalCodeBytes / 1024:F1} KB",
+ _ => $"{TotalCodeBytes} B"
+ };
+}
+
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; }
+ public required List OverallLanguages { get; init; }
+ public required List PersonalRepos { get; init; }
+ public required TeamStats? TeamStats { get; init; }
}
diff --git a/src/ProfileManager/Program.cs b/src/ProfileManager/Program.cs
index 1ec04be..925f8eb 100644
--- a/src/ProfileManager/Program.cs
+++ b/src/ProfileManager/Program.cs
@@ -11,85 +11,96 @@ internal static class Program
Console.WriteLine("""
====================================================
🦊 NickiCloud Gitea Profile Manager (.NET 10)
- Zero-Dependency Autonomous Activity Engine
+ Enterprise Team & Activity Engine v2.0
====================================================
""");
- // 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 teamSvgPath = GetConfigValue(args, "--team-svg", "TEAM_SVG", "team-stats.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));
- }
+ var repoRoot = FindRepoRoot();
+ if (!Path.IsPathRooted(svgPath)) svgPath = Path.GetFullPath(Path.Combine(repoRoot, svgPath));
+ if (!Path.IsPathRooted(teamSvgPath)) teamSvgPath = Path.GetFullPath(Path.Combine(repoRoot, teamSvgPath));
+ if (!Path.IsPathRooted(readmePath)) readmePath = Path.GetFullPath(Path.Combine(repoRoot, readmePath));
- 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($"[Config] Target User : {username}");
+ Console.WriteLine($"[Config] API Endpoint : {apiUrl}");
+ Console.WriteLine($"[Config] Heatmap SVG : {svgPath}");
+ Console.WriteLine($"[Config] Team Stats SVG : {teamSvgPath}");
+ 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
+ // Step 1: Parallel retrieval of Heatmap, Accessible Repos, and Organizations
var heatmapTask = client.GetHeatmapAsync(username);
- var reposTask = client.GetReposAsync(username);
+ var reposTask = client.GetAllAccessibleReposAsync(username);
+ var orgsTask = client.GetUserOrganizationsAsync(username);
- await Task.WhenAll(heatmapTask, reposTask);
+ await Task.WhenAll(heatmapTask, reposTask, orgsTask);
var heatmap = await heatmapTask;
var repos = await reposTask;
+ var orgs = await orgsTask;
- // Step 2: Calculate metrics
- Console.WriteLine("[Stats] Calculating streaks, contributions and language metrics...");
- var stats = StatsCalculator.Calculate(heatmap, repos);
+ // Step 2: Fetch detailed language byte counts per repository
+ Console.WriteLine("[API] Fetching byte-level language breakdown for all repositories...");
+ var langTasks = repos.Select(async repo =>
+ {
+ var owner = repo.Owner?.Login ?? (repo.FullName.Contains('/') ? repo.FullName.Split('/')[0] : username);
+ repo.DetailedLanguages = await client.GetRepoLanguagesAsync(owner, repo.Name);
+ });
+ await Task.WhenAll(langTasks);
- 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))}");
+ // Step 3: Calculate multi-tier statistics
+ Console.WriteLine("[Stats] Calculating Personal, Team & Byte metrics...");
+ var stats = StatsCalculator.Calculate(heatmap, repos, orgs, username);
+
+ Console.WriteLine($"[Stats] 365d Contributions: {stats.TotalContributions365}");
+ Console.WriteLine($"[Stats] Current Streak : {stats.CurrentStreak} days");
+ Console.WriteLine($"[Stats] Longest Streak : {stats.LongestStreak} days");
+ Console.WriteLine($"[Stats] Personal Repos : {stats.PersonalRepos.Count}");
+ if (stats.TeamStats != null)
+ {
+ Console.WriteLine($"[Stats] Team Org : {stats.TeamStats.OrganizationName}");
+ Console.WriteLine($"[Stats] Team Repos : {stats.TeamStats.TotalRepos}");
+ Console.WriteLine($"[Stats] Team Code Volume : {stats.TeamStats.FormattedTotalSize}");
+ Console.WriteLine($"[Stats] Team Top Languages: {string.Join(", ", stats.TeamStats.Languages.Select(l => $"{l.Name}: {l.Percentage:F1}% ({l.FormattedSize})"))}");
+ }
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);
- }
var utf8WithoutBom = new UTF8Encoding(false);
- await File.WriteAllTextAsync(svgPath, svgContent, utf8WithoutBom);
- Console.WriteLine($"[SVG] Saved successfully to: {svgPath}");
- // Step 4: Inject latest repos into README.md
+ // Step 4: Generate Heatmap SVG
+ Console.WriteLine($"[SVG] Rendering Heatmap SVG: {svgPath}");
+ var heatmapSvg = SvgGenerator.GenerateHeatmap(stats);
+ EnsureDirectory(svgPath);
+ await File.WriteAllTextAsync(svgPath, heatmapSvg, utf8WithoutBom);
+
+ // Step 5: Generate Team Stats SVG
+ if (stats.TeamStats != null)
+ {
+ Console.WriteLine($"[SVG] Rendering Team Stats SVG: {teamSvgPath}");
+ var teamSvg = SvgGenerator.GenerateTeamStats(stats.TeamStats);
+ EnsureDirectory(teamSvgPath);
+ await File.WriteAllTextAsync(teamSvgPath, teamSvg, utf8WithoutBom);
+ }
+
+ // Step 6: Inject all sections 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.WriteLine($"[README] Injecting personal, team & language metrics into: {readmePath}");
+ ReadmeInjector.InjectAll(readmePath, stats);
}
Console.ForegroundColor = ConsoleColor.Green;
- Console.WriteLine("\n✨ Profile update finished successfully!");
+ Console.WriteLine("\n✨ Multi-tier profile update completed successfully!");
Console.ResetColor();
return 0;
}
@@ -103,6 +114,15 @@ internal static class Program
}
}
+ private static void EnsureDirectory(string filePath)
+ {
+ var dir = Path.GetDirectoryName(filePath);
+ if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
+ {
+ Directory.CreateDirectory(dir);
+ }
+ }
+
private static string GetConfigValue(string[] args, string flag, string envVar, string defaultValue)
{
for (int i = 0; i < args.Length - 1; i++)
diff --git a/src/ProfileManager/Services/GiteaApiClient.cs b/src/ProfileManager/Services/GiteaApiClient.cs
index ebcac5c..58a5b46 100644
--- a/src/ProfileManager/Services/GiteaApiClient.cs
+++ b/src/ProfileManager/Services/GiteaApiClient.cs
@@ -8,6 +8,7 @@ public sealed class GiteaApiClient
{
private readonly HttpClient _httpClient;
private readonly string _baseUrl;
+ private readonly bool _hasToken;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
@@ -29,12 +30,13 @@ public sealed class GiteaApiClient
Timeout = TimeSpan.FromSeconds(15)
};
- _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("NickiProfileBot", "1.0"));
+ _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("NickiProfileBot", "2.0"));
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var token = apiToken ?? Environment.GetEnvironmentVariable("GITEA_TOKEN");
if (!string.IsNullOrWhiteSpace(token))
{
+ _hasToken = true;
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
}
@@ -53,28 +55,31 @@ public sealed class GiteaApiClient
var items = JsonSerializer.Deserialize>(json, JsonOptions);
if (items != null && items.Count > 0)
{
- Console.WriteLine($"[API] Successfully retrieved {items.Count} heatmap data points.");
+ Console.WriteLine($"[API] Heatmap: {items.Count} entries retrieved.");
return items;
}
}
-
Console.ForegroundColor = ConsoleColor.Yellow;
- Console.WriteLine($"[API-WARN] Heatmap request failed (Status: {response.StatusCode}). Switching to mock fallback.");
+ Console.WriteLine($"[API-WARN] Heatmap status: {response.StatusCode}. Using 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.WriteLine($"[API-WARN] Heatmap error: {ex.Message}. Using fallback.");
Console.ResetColor();
}
return GenerateMockHeatmap();
}
- public async Task> GetReposAsync(string username, CancellationToken cancellationToken = default)
+ public async Task> GetAllAccessibleReposAsync(string username, CancellationToken cancellationToken = default)
{
- var url = $"{_baseUrl}/users/{Uri.EscapeDataString(username)}/repos?limit=100&sort=updated";
+ // If authenticated, /user/repos retrieves all personal AND team/organization repos
+ var url = _hasToken
+ ? $"{_baseUrl}/user/repos?limit=100&sort=updated"
+ : $"{_baseUrl}/users/{Uri.EscapeDataString(username)}/repos?limit=100&sort=updated";
+
Console.WriteLine($"[API] Fetching repositories from: {url}");
try
@@ -86,45 +91,109 @@ public sealed class GiteaApiClient
var repos = JsonSerializer.Deserialize>(json, JsonOptions);
if (repos != null && repos.Count > 0)
{
- Console.WriteLine($"[API] Successfully retrieved {repos.Count} repositories.");
+ Console.WriteLine($"[API] Retrieved {repos.Count} total repositories.");
return repos;
}
}
-
Console.ForegroundColor = ConsoleColor.Yellow;
- Console.WriteLine($"[API-WARN] Repositories request failed (Status: {response.StatusCode}). Switching to mock fallback.");
+ Console.WriteLine($"[API-WARN] Repos request status: {response.StatusCode}. Using 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.WriteLine($"[API-WARN] Repos error: {ex.Message}. Using fallback.");
Console.ResetColor();
}
return GenerateMockRepos();
}
+ public async Task> GetUserOrganizationsAsync(string username, CancellationToken cancellationToken = default)
+ {
+ var url = _hasToken
+ ? $"{_baseUrl}/user/orgs"
+ : $"{_baseUrl}/users/{Uri.EscapeDataString(username)}/orgs";
+
+ Console.WriteLine($"[API] Fetching organizations from: {url}");
+
+ try
+ {
+ using var response = await _httpClient.GetAsync(url, cancellationToken);
+ if (response.IsSuccessStatusCode)
+ {
+ var json = await response.Content.ReadAsStringAsync(cancellationToken);
+ var orgs = JsonSerializer.Deserialize>(json, JsonOptions);
+ if (orgs != null && orgs.Count > 0)
+ {
+ Console.WriteLine($"[API] Retrieved {orgs.Count} organization(s): {string.Join(", ", orgs.Select(o => o.Name))}");
+ return orgs;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"[API-WARN] Orgs error: {ex.Message}.");
+ Console.ResetColor();
+ }
+
+ return
+ [
+ new GiteaOrganization
+ {
+ Id = 3,
+ Name = "WebCore",
+ Username = "WebCore",
+ FullName = "WebCore Consulting",
+ Website = "https://www.webcore-consulting.de",
+ Location = "NRW"
+ }
+ ];
+ }
+
+ public async Task> GetRepoLanguagesAsync(string owner, string repoName, CancellationToken cancellationToken = default)
+ {
+ var url = $"{_baseUrl}/repos/{Uri.EscapeDataString(owner)}/{Uri.EscapeDataString(repoName)}/languages";
+ try
+ {
+ using var response = await _httpClient.GetAsync(url, cancellationToken);
+ if (response.IsSuccessStatusCode)
+ {
+ var json = await response.Content.ReadAsStringAsync(cancellationToken);
+ var langs = JsonSerializer.Deserialize>(json, JsonOptions);
+ if (langs != null)
+ {
+ return langs;
+ }
+ }
+ }
+ catch
+ {
+ // Ignore single repo language errors and return empty
+ }
+
+ return [];
+ }
+
private static List GenerateMockHeatmap()
{
var items = new List();
var now = DateTimeOffset.UtcNow;
- var random = new Random(42); // Fixed seed for reproducible fallback
+ var random = new Random(42);
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;
+ int chance = isWeekend ? 35 : 70;
if (random.Next(100) < chance)
{
- int commits = random.Next(1, 14);
items.Add(new HeatmapItem
{
Timestamp = day.ToUnixTimeSeconds(),
- Contributions = commits
+ Contributions = random.Next(1, 12)
});
}
}
@@ -139,69 +208,47 @@ public sealed class GiteaApiClient
[
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",
+ Name = "USBSafe",
+ FullName = "WebCore/USBSafe",
+ Description = "Sicherheits- & Verschlüsselungs-Tool für Wechseldatenträger im Unternehmensnetzwerk.",
+ HtmlUrl = "https://git.webcore-consulting.dev/WebCore/USBSafe",
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)
+ UpdatedAt = now.AddHours(-1),
+ Owner = new RepoOwner { Login = "WebCore", FullName = "WebCore Consulting" }
+ },
+ new GiteaRepo
+ {
+ Name = "ServerManagement",
+ FullName = "WebCore/ServerManagement",
+ Description = "Internes Verwaltungs- & Monitoring-System für ServerHub mit MudBlazor & ASP.NET Core.",
+ HtmlUrl = "https://git.webcore-consulting.dev/WebCore/ServerManagement",
+ Language = "HTML",
+ StarsCount = 3,
+ UpdatedAt = now.AddHours(-4),
+ Owner = new RepoOwner { Login = "WebCore", FullName = "WebCore Consulting" }
+ },
+ new GiteaRepo
+ {
+ Name = "ServerHub-redesign",
+ FullName = "WebCore/ServerHub-redesign",
+ Description = "Modernes Frontend-Redesign für das zentrale WebCore-Kundenportal.",
+ HtmlUrl = "https://git.webcore-consulting.dev/WebCore/ServerHub-redesign",
+ Language = "C#",
+ StarsCount = 1,
+ UpdatedAt = now.AddDays(-3),
+ Owner = new RepoOwner { Login = "WebCore", FullName = "WebCore Consulting" }
+ },
+ new GiteaRepo
+ {
+ Name = ".profile",
+ FullName = "NickiCloud/.profile",
+ Description = "🦊 Gitea-Profil & dynamische Aktivitäts-Statistiken mit .NET 10",
+ HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/.profile",
+ Language = "C#",
+ StarsCount = 1,
+ UpdatedAt = now,
+ Owner = new RepoOwner { Login = "NickiCloud", FullName = "Nicklas Voß" }
}
];
}
diff --git a/src/ProfileManager/Services/ReadmeInjector.cs b/src/ProfileManager/Services/ReadmeInjector.cs
index a2b510a..9ff9c66 100644
--- a/src/ProfileManager/Services/ReadmeInjector.cs
+++ b/src/ProfileManager/Services/ReadmeInjector.cs
@@ -1,15 +1,20 @@
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 = "";
+ private const string StartReposMarker = "";
+ private const string EndReposMarker = "";
- public static bool InjectLatestRepos(string readmePath, List repos)
+ private const string StartTeamReposMarker = "";
+ private const string EndTeamReposMarker = "";
+
+ private const string StartLangMarker = "";
+ private const string EndLangMarker = "";
+
+ public static bool InjectAll(string readmePath, ProfileStats stats)
{
if (!File.Exists(readmePath))
{
@@ -20,52 +25,97 @@ public static class ReadmeInjector
}
var content = File.ReadAllText(readmePath, Encoding.UTF8);
- int startIndex = content.IndexOf(StartMarker, StringComparison.Ordinal);
- int endIndex = content.IndexOf(EndMarker, StringComparison.Ordinal);
+
+ // 1. Inject Personal Repos
+ content = ReplaceSection(content, StartReposMarker, EndReposMarker, sb =>
+ {
+ if (stats.PersonalRepos.Count == 0)
+ {
+ sb.AppendLine("*Momentan keine öffentlichen persönlichen Repositories vorhanden.*");
+ }
+ else
+ {
+ foreach (var repo in stats.PersonalRepos.Take(3))
+ {
+ 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");
+
+ sb.AppendLine($"- 🚀 [**{repo.Name}**]({repo.HtmlUrl}) – {desc} ");
+ sb.AppendLine($" `{lang}`{stars} • 🕒 *Aktualisiert am {updated}*");
+ }
+ }
+ });
+
+ // 2. Inject Team Repos
+ content = ReplaceSection(content, StartTeamReposMarker, EndTeamReposMarker, sb =>
+ {
+ if (stats.TeamStats == null || stats.TeamStats.Repos.Count == 0)
+ {
+ sb.AppendLine("*Keine Team-Repositories zugewiesen.*");
+ }
+ else
+ {
+ foreach (var repo in stats.TeamStats.Repos.Take(4))
+ {
+ var desc = string.IsNullOrWhiteSpace(repo.Description)
+ ? "WebCore Consulting Projekt"
+ : repo.Description.Trim();
+
+ var lang = string.IsNullOrWhiteSpace(repo.Language) ? "C# / Web" : repo.Language;
+ var updated = repo.UpdatedAt.ToString("dd.MM.yyyy");
+ var isPrivate = repo.Private ? " 🔒" : string.Empty;
+
+ sb.AppendLine($"- 🏢 [**{repo.FullName}**]({repo.HtmlUrl}){isPrivate} – {desc} ");
+ sb.AppendLine($" `{lang}` • 🕒 *Zuletzt bearbeitet: {updated}*");
+ }
+ }
+ });
+
+ // 3. Inject Language Metrics Table / Progress Bars
+ content = ReplaceSection(content, StartLangMarker, EndLangMarker, sb =>
+ {
+ var langs = stats.TeamStats?.Languages ?? stats.OverallLanguages;
+ sb.AppendLine("| Sprache | Anteil | Code-Volumen | Verteilung |");
+ sb.AppendLine("| :--- | :---: | :---: | :--- |");
+
+ foreach (var l in langs.Take(6))
+ {
+ int filledBlocks = (int)Math.Round(l.Percentage / 5.0);
+ string bar = new string('█', filledBlocks).PadRight(20, '░');
+ sb.AppendLine($"| `{l.Name}` | **{l.Percentage:F1}%** | {l.FormattedSize} | `{bar}` |");
+ }
+ });
+
+ var utf8WithoutBom = new UTF8Encoding(false);
+ File.WriteAllText(readmePath, content, utf8WithoutBom);
+ Console.WriteLine($"[README] Successfully updated all dynamic sections in {readmePath}");
+ return true;
+ }
+
+ private static string ReplaceSection(string source, string startMarker, string endMarker, Action builderAction)
+ {
+ int startIndex = source.IndexOf(startMarker, StringComparison.Ordinal);
+ int endIndex = source.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;
+ return source;
}
- var replacement = new StringBuilder();
- replacement.AppendLine(StartMarker);
+ var sb = new StringBuilder();
+ sb.AppendLine(startMarker);
+ builderAction(sb);
+ sb.Append(endMarker);
- 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);
-
- var utf8WithoutBom = new UTF8Encoding(false);
- File.WriteAllText(readmePath, newContent, utf8WithoutBom);
- Console.WriteLine($"[README] Successfully injected {repos.Count} repositories into {readmePath}");
- return true;
+ return source.Substring(0, startIndex) + sb + source.Substring(endIndex + endMarker.Length);
}
}
diff --git a/src/ProfileManager/Services/StatsCalculator.cs b/src/ProfileManager/Services/StatsCalculator.cs
index 1bade9d..35338de 100644
--- a/src/ProfileManager/Services/StatsCalculator.cs
+++ b/src/ProfileManager/Services/StatsCalculator.cs
@@ -4,33 +4,34 @@ namespace ProfileManager.Services;
public static class StatsCalculator
{
- private static readonly Dictionary LanguageColors = new(StringComparer.OrdinalIgnoreCase)
+ public static readonly Dictionary LanguageColors = new(StringComparer.OrdinalIgnoreCase)
{
{ "C#", "#a855f7" },
- { "C++", "#f34b7d" },
- { "C", "#555555" },
{ "HTML", "#e34c26" },
{ "CSS", "#563d7c" },
{ "JavaScript", "#f1e05a" },
{ "TypeScript", "#3178c6" },
- { "Python", "#3572A5" },
+ { "PowerShell", "#012456" },
{ "Shell", "#89e051" },
{ "Bash", "#89e051" },
{ "Dockerfile", "#384d54" },
+ { "Python", "#3572A5" },
{ "Go", "#00ADD8" },
{ "Rust", "#dea584" },
{ "Vue", "#41b883" },
- { "PHP", "#4F5D95" },
- { "Kotlin", "#A97BFF" },
- { "Java", "#b07219" },
- { "Dart", "#00B4AB" }
+ { "XAML", "#b53d9e" }
};
- public static ProfileStats Calculate(List heatmapItems, List repos, DateOnly? referenceDate = null)
+ public static ProfileStats Calculate(
+ List heatmapItems,
+ List allRepos,
+ List orgs,
+ string personalUsername = "NickiCloud",
+ DateOnly? referenceDate = null)
{
var today = referenceDate ?? DateOnly.FromDateTime(DateTime.UtcNow);
- // 1. Group heatmap contributions by DateOnly (UTC)
+ // 1. Heatmap Contributions Map
var dateMap = new Dictionary();
foreach (var item in heatmapItems)
{
@@ -38,7 +39,7 @@ public static class StatsCalculator
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)
+ // 2. 52x7 Heatmap Grid
int dayOffsetFromMonday = ((int)today.DayOfWeek + 6) % 7;
var gridEndSunday = today.AddDays(6 - dayOffsetFromMonday);
var gridStartMonday = gridEndSunday.AddDays(-(52 * 7 - 1));
@@ -70,13 +71,10 @@ public static class StatsCalculator
}
}
- // 3. Total contributions (last 365 days)
+ // 3. Totals and Streaks
var oneYearAgo = today.AddDays(-365);
- int total365 = dateMap
- .Where(kv => kv.Key >= oneYearAgo && kv.Key <= today)
- .Sum(kv => kv.Value);
+ 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)
{
@@ -90,7 +88,6 @@ public static class StatsCalculator
}
else
{
- // If today has no commit yet, yesterday's streak is still active
var check = today.AddDays(-1);
while (dateMap.GetValueOrDefault(check, 0) > 0)
{
@@ -99,7 +96,6 @@ public static class StatsCalculator
}
}
- // Longest Streak across all dates
int longestStreak = 0;
if (dateMap.Count > 0)
{
@@ -121,50 +117,41 @@ public static class StatsCalculator
}
}
- // 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
+ // 4. Split Personal vs. Team Repos
+ var personalRepos = allRepos
+ .Where(r => !r.IsOrganizationRepo(personalUsername))
.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)
+ var teamRepos = allRepos
+ .Where(r => r.IsOrganizationRepo(personalUsername))
+ .OrderByDescending(r => r.UpdatedAt)
.ToList();
- int totalLanguageRepos = languageGroups.Sum(x => x.Count);
- var topLanguages = new List();
+ // 5. Calculate Languages (from Byte Breakdown)
+ var overallLanguages = AggregateLanguages(allRepos);
+ var teamLanguages = AggregateLanguages(teamRepos);
- if (totalLanguageRepos > 0)
+ // 6. Build TeamStats
+ TeamStats? teamStats = null;
+ if (teamRepos.Count > 0 || orgs.Count > 0)
{
- foreach (var lang in languageGroups.Take(5))
+ var mainOrg = orgs.FirstOrDefault();
+ string orgName = mainOrg?.FullName ?? mainOrg?.Name ?? "WebCore Consulting";
+ string orgLogin = mainOrg?.Username ?? "WebCore";
+ string website = string.IsNullOrWhiteSpace(mainOrg?.Website) ? "https://www.webcore-consulting.de" : mainOrg.Website;
+
+ long totalBytes = teamLanguages.Sum(l => l.Bytes);
+ teamStats = new TeamStats
{
- 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"
- });
+ OrganizationName = orgName,
+ OrganizationLogin = orgLogin,
+ Website = website,
+ TotalRepos = teamRepos.Count,
+ TotalCodeBytes = totalBytes,
+ Languages = teamLanguages,
+ Repos = teamRepos
+ };
}
return new ProfileStats
@@ -173,8 +160,65 @@ public static class StatsCalculator
CurrentStreak = currentStreak,
LongestStreak = Math.Max(longestStreak, currentStreak),
Days = days,
- TopLanguages = topLanguages,
- LatestRepos = latestRepos
+ OverallLanguages = overallLanguages,
+ PersonalRepos = personalRepos,
+ TeamStats = teamStats
};
}
+
+ private static List AggregateLanguages(List repos)
+ {
+ var byteMap = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var countMap = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var repo in repos)
+ {
+ if (repo.DetailedLanguages.Count > 0)
+ {
+ foreach (var (lang, bytes) in repo.DetailedLanguages)
+ {
+ byteMap[lang] = byteMap.GetValueOrDefault(lang, 0L) + bytes;
+ countMap[lang] = countMap.GetValueOrDefault(lang, 0) + 1;
+ }
+ }
+ else if (!string.IsNullOrWhiteSpace(repo.Language))
+ {
+ // Fallback estimate if no byte details: 100KB per repo
+ byteMap[repo.Language] = byteMap.GetValueOrDefault(repo.Language, 0L) + 100_000L;
+ countMap[repo.Language] = countMap.GetValueOrDefault(repo.Language, 0) + 1;
+ }
+ }
+
+ long totalBytes = byteMap.Values.Sum();
+ var result = new List();
+
+ if (totalBytes > 0)
+ {
+ foreach (var (lang, bytes) in byteMap.OrderByDescending(kv => kv.Value))
+ {
+ double pct = Math.Round((double)bytes / totalBytes * 100.0, 1);
+ result.Add(new LanguageStat
+ {
+ Name = lang,
+ Percentage = pct,
+ Bytes = bytes,
+ RepoCount = countMap.GetValueOrDefault(lang, 1),
+ Color = LanguageColors.GetValueOrDefault(lang, "#c084fc")
+ });
+ }
+ }
+ else
+ {
+ result.Add(new LanguageStat
+ {
+ Name = "C#",
+ Percentage = 100.0,
+ Bytes = 500_000L,
+ RepoCount = 1,
+ Color = "#a855f7"
+ });
+ }
+
+ return result;
+ }
}
diff --git a/src/ProfileManager/Services/SvgGenerator.cs b/src/ProfileManager/Services/SvgGenerator.cs
index 6f2d1f2..7960040 100644
--- a/src/ProfileManager/Services/SvgGenerator.cs
+++ b/src/ProfileManager/Services/SvgGenerator.cs
@@ -11,7 +11,7 @@ public static class SvgGenerator
"Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"
];
- public static string Generate(ProfileStats stats)
+ public static string GenerateHeatmap(ProfileStats stats)
{
var sb = new StringBuilder(16384);
@@ -104,9 +104,8 @@ public static class SvgGenerator
// Header: Title & Total Commits Badge
sb.AppendLine(" ");
sb.AppendLine(" ");
- sb.AppendLine(" 🦊 NickiCloud • Gitea Activity ");
+ sb.AppendLine(" 🦊 NickiCloud • Persönliche Aktivität ");
- // Header badge (right aligned)
string badgeText = $"{stats.TotalContributions365:N0} Commits im letzten Jahr";
sb.AppendLine($" ");
sb.AppendLine($" ");
@@ -122,7 +121,6 @@ public static class SvgGenerator
if (firstDayOfCol != null && firstDayOfCol.Date.Month != lastMonth)
{
lastMonth = firstDayOfCol.Date.Month;
- // Avoid rendering month if too close to right edge
if (col < 50)
{
int x = gridStartX + col * colStep;
@@ -131,7 +129,7 @@ public static class SvgGenerator
}
}
- // Day of Week Labels (Mo, Mi, Fr)
+ // Weekday Labels (Mo, Mi, Fr)
sb.AppendLine(" ");
sb.AppendLine($" Mo ");
sb.AppendLine($" Mi ");
@@ -160,13 +158,8 @@ public static class SvgGenerator
int cardWidth = 216;
int gap = 14;
- // Card 1: Total
RenderMetricCard(sb, gridStartX, cardY, cardWidth, cardHeight, "📦", $"{stats.TotalContributions365:N0}", "Jahres-Beiträge");
-
- // Card 2: Current Streak
RenderMetricCard(sb, gridStartX + cardWidth + gap, cardY, cardWidth, cardHeight, "🔥", $"{stats.CurrentStreak} {(stats.CurrentStreak == 1 ? "Tag" : "Tage")}", "Aktueller Streak");
-
- // Card 3: Longest Streak
RenderMetricCard(sb, gridStartX + (cardWidth + gap) * 2, cardY, cardWidth, cardHeight, "⚡", $"{stats.LongestStreak} {(stats.LongestStreak == 1 ? "Tag" : "Tage")}", "Längster Streak");
// Top Languages Bar (width = 676px)
@@ -175,7 +168,7 @@ public static class SvgGenerator
double currentX = gridStartX;
const double totalBarWidth = 676.0;
- foreach (var lang in stats.TopLanguages)
+ foreach (var lang in stats.OverallLanguages.Take(5))
{
double w = Math.Max(2.0, (lang.Percentage / 100.0) * totalBarWidth);
sb.AppendLine($" ");
@@ -189,7 +182,7 @@ public static class SvgGenerator
sb.AppendLine(" ");
sb.AppendLine(" ");
int legendX = 0;
- foreach (var lang in stats.TopLanguages)
+ foreach (var lang in stats.OverallLanguages.Take(5))
{
sb.AppendLine($" ");
string text = $"{lang.Name} {lang.Percentage:F1}%";
@@ -202,13 +195,143 @@ public static class SvgGenerator
return sb.ToString();
}
+ public static string GenerateTeamStats(TeamStats team)
+ {
+ var sb = new StringBuilder(16384);
+
+ const int svgWidth = 740;
+ const int svgHeight = 240;
+ const int gridStartX = 34;
+ const double totalBarWidth = 676.0;
+
+ sb.AppendLine("""""");
+ sb.AppendLine($"""""");
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+ sb.AppendLine($" ");
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+
+ sb.AppendLine("""
+
+ """);
+
+ // Background Panel
+ sb.AppendLine($" ");
+
+ // Header: Team Title & Badge
+ sb.AppendLine(" ");
+ sb.AppendLine($" 🏢 {team.OrganizationName} • Team & Projekt-Statistiken ");
+
+ string badgeText = $"{team.TotalRepos} Team-Repositories";
+ sb.AppendLine($" ");
+ sb.AppendLine($" ");
+ sb.AppendLine($" {badgeText} ");
+ sb.AppendLine(" ");
+
+ // Metric Badges (Row 1)
+ int cardY = 56;
+ int cardHeight = 44;
+ int cardWidth = 216;
+ int gap = 14;
+
+ var topLang = team.Languages.FirstOrDefault();
+ string primaryLangStr = topLang != null ? $"{topLang.Name} ({topLang.Percentage:F1}%)" : "C#";
+
+ RenderMetricCard(sb, gridStartX, cardY, cardWidth, cardHeight, "📁", $"{team.TotalRepos} Repos", "Aktive Team-Projekte");
+ RenderMetricCard(sb, gridStartX + cardWidth + gap, cardY, cardWidth, cardHeight, "💾", team.FormattedTotalSize, "Gesamter Team-Code");
+ RenderMetricCard(sb, gridStartX + (cardWidth + gap) * 2, cardY, cardWidth, cardHeight, "⚡", primaryLangStr, "Hauptsprache im Team");
+
+ // Team Languages Bar
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+ double currentX = gridStartX;
+ foreach (var lang in team.Languages)
+ {
+ double w = Math.Max(3.0, (lang.Percentage / 100.0) * totalBarWidth);
+ sb.AppendLine($" ");
+ sb.AppendLine($" {lang.Name}: {lang.Percentage:F1}% ({lang.FormattedSize}) ");
+ sb.AppendLine(" ");
+ currentX += w;
+ }
+ sb.AppendLine(" ");
+
+ // Team Languages Legend (Detailed with Byte Sizes)
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+ int legendX = 0;
+ int legendY = 0;
+ int count = 0;
+
+ foreach (var lang in team.Languages.Take(6))
+ {
+ sb.AppendLine($" ");
+ string text = $"{lang.Name}: {lang.Percentage:F1}% ({lang.FormattedSize})";
+ sb.AppendLine($" {text} ");
+
+ count++;
+ if (count % 3 == 0)
+ {
+ legendX = 0;
+ legendY += 18;
+ }
+ else
+ {
+ legendX += 225;
+ }
+ }
+ sb.AppendLine(" ");
+
+ // Bottom active repos pills
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+ string repoPills = string.Join(" • ", team.Repos.Take(3).Select(r => $"🚀 {r.Name} ({r.Language ?? "Code"})"));
+ sb.AppendLine($" Team-Repositories: {repoPills} ");
+ sb.AppendLine(" ");
+
+ 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($" {icon} ");
+ sb.AppendLine($" {value} ");
+ sb.AppendLine($" {label} ");
sb.AppendLine(" ");
}
}
diff --git a/team-stats.svg b/team-stats.svg
new file mode 100644
index 0000000..470eda2
--- /dev/null
+++ b/team-stats.svg
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+ 🏢 WebCore Consulting • Team & Projekt-Statistiken
+
+
+ 3 Team-Repositories
+
+
+
+ 📁
+ 3 Repos
+ Aktive Team-Projekte
+
+
+
+ 💾
+ 1,9 MB
+ Gesamter Team-Code
+
+
+
+ ⚡
+ C# (56,2%)
+ Hauptsprache im Team
+
+
+
+
+ C#: 56,2% (1,1 MB)
+
+
+ HTML: 40,4% (797,4 KB)
+
+
+ CSS: 2,3% (45,7 KB)
+
+
+ JavaScript: 0,4% (8,6 KB)
+
+
+ PowerShell: 0,4% (7,6 KB)
+
+
+ Shell: 0,2% (3,4 KB)
+
+
+ Dockerfile: 0,1% (2,4 KB)
+
+
+
+
+
+ C#: 56,2% (1,1 MB)
+
+ HTML: 40,4% (797,4 KB)
+
+ CSS: 2,3% (45,7 KB)
+
+ JavaScript: 0,4% (8,6 KB)
+
+ PowerShell: 0,4% (7,6 KB)
+
+ Shell: 0,2% (3,4 KB)
+
+
+
+ Team-Repositories: 🚀 ServerManagement (HTML) • 🚀 USBSafe (C#) • 🚀 ServerHub-redesign ()
+
+