From c4c47ab445f340d1235011f6de5798d2a5f8ce04 Mon Sep 17 00:00:00 2001 From: NickiCloud Date: Wed, 2 Sep 2026 21:15:59 +0200 Subject: [PATCH] feat: add productivity biorhythm clock, developer achievements and quote [skip ci] --- .gitea/workflows/update-profile.yml | 5 +- README.md | 63 ++++++++--- heatmap.svg | 30 ++--- productivity.svg | 83 ++++++++++++++ src/ProfileManager/Models/ProfileStats.cs | 15 +++ src/ProfileManager/Program.cs | 26 +++-- src/ProfileManager/Services/ReadmeInjector.cs | 39 ++++++- .../Services/StatsCalculator.cs | 73 +++++++++++- src/ProfileManager/Services/SvgGenerator.cs | 104 ++++++++++++++++++ 9 files changed, 398 insertions(+), 40 deletions(-) create mode 100644 productivity.svg diff --git a/.gitea/workflows/update-profile.yml b/.gitea/workflows/update-profile.yml index f927d9a..ef63985 100644 --- a/.gitea/workflows/update-profile.yml +++ b/.gitea/workflows/update-profile.yml @@ -13,6 +13,7 @@ on: paths-ignore: - 'heatmap.svg' - 'team-stats.svg' + - 'productivity.svg' - 'README.md' jobs: @@ -50,9 +51,9 @@ jobs: git config --global user.email "bot@webcore-consulting.dev" # 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 + if git status --porcelain | grep -E "(heatmap.svg|team-stats.svg|productivity.svg|README.md)"; then echo "Änderungen an Grafiken oder README.md erkannt. Committe..." - git add heatmap.svg team-stats.svg README.md + git add heatmap.svg team-stats.svg productivity.svg README.md # GANZ WICHTIG: [skip ci] verhindert Endlosschleifen in Gitea Actions! git commit -m "chore(profile): update personal & team telemetry [skip ci]" diff --git a/README.md b/README.md index 754dae6..4578b06 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Gitea Online Debian 12 Runner Active - Last Sync + Last Sync

@@ -115,11 +115,11 @@ Ich bin **Nicki (Nicklas)**, angehender Softwareentwickler mit fundiertem Fokus > *Echtzeit-Feed der letzten Aktionen und Commits über alle privaten & Team-Repositories.* -- ⚡ **feat: add team telemetry dashboard, byte-level language analytics and tabs [skip ci]** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) • 🕒 *vor 2 Minuten* -- ⚡ **fix(svg): remove utf-8 bom and add xml declaration for gitea typesniffer [skip ci]** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) • 🕒 *vor 10 Minuten* -- ⚡ **chore(profile): update activity heatmap & stats (22 contributions) [skip ci]** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) • 🕒 *vor 18 Minuten* -- ⚡ **Initial profile setup** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) • 🕒 *vor 19 Minuten* -- ✨ **Repository neu erstellt** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) • 🕒 *vor 55 Minuten* +- ⚡ **feat: add live commit radar, activity feed and homelab telemetry [skip ci]** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) • 🕒 *vor 2 Minuten* +- ⚡ **feat: add team telemetry dashboard, byte-level language analytics and tabs [skip ci]** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) • 🕒 *vor 6 Minuten* +- ⚡ **fix(svg): remove utf-8 bom and add xml declaration for gitea typesniffer [skip ci]** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) • 🕒 *vor 14 Minuten* +- ⚡ **chore(profile): update activity heatmap & stats (22 contributions) [skip ci]** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) • 🕒 *vor 21 Minuten* +- ⚡ **Initial profile setup** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) • 🕒 *vor 22 Minuten* @@ -127,7 +127,37 @@ Ich bin **Nicki (Nicklas)**, angehender Softwareentwickler mit fundiertem Fokus
-

🛡️ Tab 4: Homelab, Server-Infrastruktur & Telemetrie

+

⏱️ Tab 4: Coding Biorhythmus, Produktivität & Trophäen

+ +> *Analyse der aktiven Entwicklungsphasen, Trophäen und Entwickler-Weisheiten.* + +
+ Nicki's Coding Productivity Clock +
+ +#### 🏆 Entwickler-Erfolge & Achievements: + +| Trophy | Rang | Beschreibung | Status | +| :--- | :---: | :--- | :---: | +| 🏆 **Streak Master** | `Gold` | Mindestens 4 Tage ununterbrochen aktiv gecodet | **4 Tage** ✅ | +| 💾 **Code Sovereign** | `Diamond` | Über 1,5 MB Code in Gitea verwaltet | **1,9 MB** ✅ | +| ⚡ **Zero-Dependency** | `Master` | Reines .NET 10 BCL ohne fremde NuGet-Pakete | **100% Native** ✅ | +| 🌙 **Night-Owl Veteran** | `Elite` | Hohe nächtliche Coding-Aktivität | **26,3% Nachts** ✅ | +| 🏰 **Homelab Overlord** | `Legendary` | Self-Hosted Debian, Docker, Gitea, Mailcow | **Vollautonom** ✅ | + + +#### 💡 Tägliche Entwickler-Philosophie: + +> *"Clean code always looks like it was written by someone who cares."* +> — **Robert C. Martin** + + +
+ +
+ +
+

🛡️ Tab 5: Homelab, Server-Infrastruktur & Telemetrie

``` ┌─────────────────────────────────────────────────────────────┐ @@ -158,6 +188,19 @@ Ich bin **Nicki (Nicklas)**, angehender Softwareentwickler mit fundiertem Fokus
+
+ +
+

🎮 Tab 6: Gaming Battlestation & Loadouts

+ +| Game | Fokus & Rolle | Lieblingselement / Operator | +| :--- | :--- | :--- | +| ⛏️ **Minecraft** | Technical Automation & Redstone | Automatisierte Farmen, Create Mod, Server-Hosting | +| 🎯 **Rainbow Six Siege** | Tactical Anchor & Area Denial | Smoke, Mute, Thermite, Thatcher | +| ⚔️ **Warframe** | High-Level Survival & Grinding | Mesa Prime, Wisp, Volt | + +
+ --- ## 🛠️ Ausgewählte Eigenprojekte @@ -173,12 +216,6 @@ Ich bin **Nicki (Nicklas)**, angehender Softwareentwickler mit fundiertem Fokus
-### 🎮 Gaming & Fun Corner - -[![Minecraft](https://img.shields.io/badge/Minecraft-5B8C33?style=for-the-badge&logo=minecraft&logoColor=white)](https://minecraft.net) -[![Rainbow Six Siege](https://img.shields.io/badge/Rainbow_Six_Siege-000000?style=for-the-badge&logo=ubisoft&logoColor=white)](https://www.ubisoft.com/game/rainbow-six/siege) -[![Warframe](https://img.shields.io/badge/Warframe-7e3f98?style=for-the-badge&logo=target&logoColor=white)](https://www.warframe.com/) - ```csharp var nicki = new Developer { diff --git a/heatmap.svg b/heatmap.svg index 66bbbf0..76b594c 100644 --- a/heatmap.svg +++ b/heatmap.svg @@ -74,7 +74,7 @@ 🦊 NickiCloud Persönliche Aktivität - 25 Commits im letzten Jahr + 26 Commits im letzten Jahr Sep @@ -1172,8 +1172,8 @@ 01.09.2026: 9 Commits - - 02.09.2026: 9 Commits + + 02.09.2026: 10 Commits 03.09.2026: 0 Commits @@ -1192,7 +1192,7 @@ 📦 - 25 + 26 Jahres-Beiträge @@ -1209,30 +1209,30 @@ - - C#: 57,3% + + C#: 57,4% - - HTML: 39,4% + + HTML: 39,2% - - CSS: 2,3% + + CSS: 2,2% - + JavaScript: 0,4% - + PowerShell: 0,4% - C# 57,3% + C# 57,4% - HTML 39,4% + HTML 39,2% - CSS 2,3% + CSS 2,2% JavaScript 0,4% diff --git a/productivity.svg b/productivity.svg new file mode 100644 index 0000000..52314bc --- /dev/null +++ b/productivity.svg @@ -0,0 +1,83 @@ + + + + + + ⏱️ Coding Biorhythmus Aktivitäts-Phasen (CET) + + + 🌙 Night-Owl (Höchste Produktivität am Abend & nachts) + + + + 🌅 Morgen + (06:00 - 12:00) + + + 0,0% + + + + ☀️ Nachmittag + (12:00 - 18:00) + + + 5,3% + + + + 🌆 Abend + (18:00 - 24:00) + + + 68,4% + + + + 🌙 Nacht + (00:00 - 06:00) + + + 26,3% + + diff --git a/src/ProfileManager/Models/ProfileStats.cs b/src/ProfileManager/Models/ProfileStats.cs index 7341413..b630f1a 100644 --- a/src/ProfileManager/Models/ProfileStats.cs +++ b/src/ProfileManager/Models/ProfileStats.cs @@ -26,6 +26,20 @@ public sealed class DayContribution public required int Row { get; init; } // 0..6 (0=Mo..6=So) } +public sealed class ProductivityStats +{ + public required double MorningPct { get; init; } + public required double AfternoonPct { get; init; } + public required double EveningPct { get; init; } + public required double NightPct { get; init; } + public required int MorningCount { get; init; } + public required int AfternoonCount { get; init; } + public required int EveningCount { get; init; } + public required int NightCount { get; init; } + public required string MostActiveDayOfWeek { get; init; } + public required string DevArchetype { get; init; } +} + public sealed class TeamStats { public required string OrganizationName { get; init; } @@ -54,4 +68,5 @@ public sealed class ProfileStats public required List PersonalRepos { get; init; } public required TeamStats? TeamStats { get; init; } public required List RecentActivities { get; init; } + public required ProductivityStats Productivity { get; init; } } diff --git a/src/ProfileManager/Program.cs b/src/ProfileManager/Program.cs index d53759f..f4c341d 100644 --- a/src/ProfileManager/Program.cs +++ b/src/ProfileManager/Program.cs @@ -15,23 +15,28 @@ internal static class Program ==================================================== """); + var sw = System.Diagnostics.Stopwatch.StartNew(); + 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 prodSvgPath = GetConfigValue(args, "--prod-svg", "PROD_SVG", "productivity.svg"); var readmePath = GetConfigValue(args, "--readme", "README_PATH", "README.md"); 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(prodSvgPath)) prodSvgPath = Path.GetFullPath(Path.Combine(repoRoot, prodSvgPath)); if (!Path.IsPathRooted(readmePath)) readmePath = Path.GetFullPath(Path.Combine(repoRoot, 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($"[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] Productivity SVG : {prodSvgPath}"); + Console.WriteLine($"[Config] Target README : {readmePath}"); Console.WriteLine(); try @@ -94,15 +99,22 @@ internal static class Program await File.WriteAllTextAsync(teamSvgPath, teamSvg, utf8WithoutBom); } - // Step 6: Inject all sections into README.md + // Step 6: Generate Productivity SVG + Console.WriteLine($"[SVG] Rendering Productivity SVG: {prodSvgPath}"); + var prodSvg = SvgGenerator.GenerateProductivity(stats.Productivity); + EnsureDirectory(prodSvgPath); + await File.WriteAllTextAsync(prodSvgPath, prodSvg, utf8WithoutBom); + + // Step 7: Inject all sections into README.md if (File.Exists(readmePath)) { Console.WriteLine($"[README] Injecting personal, team & language metrics into: {readmePath}"); ReadmeInjector.InjectAll(readmePath, stats); } + sw.Stop(); Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("\n✨ Multi-tier profile update completed successfully!"); + Console.WriteLine($"\n✨ Multi-tier profile update completed successfully in {sw.ElapsedMilliseconds}ms!"); Console.ResetColor(); return 0; } diff --git a/src/ProfileManager/Services/ReadmeInjector.cs b/src/ProfileManager/Services/ReadmeInjector.cs index 8c2bb1a..778e7f8 100644 --- a/src/ProfileManager/Services/ReadmeInjector.cs +++ b/src/ProfileManager/Services/ReadmeInjector.cs @@ -20,6 +20,12 @@ public static class ReadmeInjector private const string StartTelemetryMarker = ""; private const string EndTelemetryMarker = ""; + private const string StartQuoteMarker = ""; + private const string EndQuoteMarker = ""; + + private const string StartAchievementsMarker = ""; + private const string EndAchievementsMarker = ""; + public static bool InjectAll(string readmePath, ProfileStats stats) { if (!File.Exists(readmePath)) @@ -139,9 +145,40 @@ public static class ReadmeInjector sb.AppendLine($"

"); }); + // 6. Inject Developer Achievements + content = ReplaceSection(content, StartAchievementsMarker, EndAchievementsMarker, sb => + { + sb.AppendLine("| Trophy | Rang | Beschreibung | Status |"); + sb.AppendLine("| :--- | :---: | :--- | :---: |"); + sb.AppendLine($"| 🏆 **Streak Master** | `Gold` | Mindestens 4 Tage ununterbrochen aktiv gecodet | **{stats.CurrentStreak} Tage** ✅ |"); + sb.AppendLine($"| 💾 **Code Sovereign** | `Diamond` | Über 1,5 MB Code in Gitea verwaltet | **{stats.TeamStats?.FormattedTotalSize ?? "1,9 MB"}** ✅ |"); + sb.AppendLine($"| ⚡ **Zero-Dependency** | `Master` | Reines .NET 10 BCL ohne fremde NuGet-Pakete | **100% Native** ✅ |"); + sb.AppendLine($"| 🌙 **Night-Owl Veteran** | `Elite` | Hohe nächtliche Coding-Aktivität | **{stats.Productivity.NightPct:F1}% Nachts** ✅ |"); + sb.AppendLine($"| 🏰 **Homelab Overlord** | `Legendary` | Self-Hosted Debian, Docker, Gitea, Mailcow | **Vollautonom** ✅ |"); + }); + + // 7. Inject Rotating Dev Wisdom Quote + content = ReplaceSection(content, StartQuoteMarker, EndQuoteMarker, sb => + { + var quotes = new[] + { + ("Measure twice, refactor once, ship autonomously.", "Nicki"), + ("Premature optimization is the root of all evil – but allocating in hot loops is a close second.", "Donald Knuth / Modern .NET"), + ("Simplicity is prerequisite for reliability.", "Edsger W. Dijkstra"), + ("First, solve the problem. Then, write the code in C#.", "John Johnson"), + ("There are only two hard things in Computer Science: cache invalidation and naming things.", "Phil Karlton"), + ("Clean code always looks like it was written by someone who cares.", "Robert C. Martin") + }; + + int dayOfYear = DateTime.UtcNow.DayOfYear; + var (text, author) = quotes[dayOfYear % quotes.Length]; + sb.AppendLine($"> *\"{text}\"* "); + sb.AppendLine($"> — **{author}**"); + }); + var utf8WithoutBom = new UTF8Encoding(false); File.WriteAllText(readmePath, content, utf8WithoutBom); - Console.WriteLine($"[README] Successfully updated all 5 dynamic sections in {readmePath}"); + Console.WriteLine($"[README] Successfully updated all 7 dynamic sections in {readmePath}"); return true; } diff --git a/src/ProfileManager/Services/StatsCalculator.cs b/src/ProfileManager/Services/StatsCalculator.cs index 0b6ab9f..bfcf2be 100644 --- a/src/ProfileManager/Services/StatsCalculator.cs +++ b/src/ProfileManager/Services/StatsCalculator.cs @@ -133,7 +133,75 @@ public static class StatsCalculator var overallLanguages = AggregateLanguages(allRepos); var teamLanguages = AggregateLanguages(teamRepos); - // 6. Build TeamStats + // 6. Productivity & Coding Biorhythm Analysis + int morningCount = 0, afternoonCount = 0, eveningCount = 0, nightCount = 0; + var dayOfWeekMap = new Dictionary(); + + foreach (var act in recentActivities) + { + var localTime = act.Created.AddHours(2); // Europe/Berlin (CEST) + int hour = localTime.Hour; + if (hour >= 6 && hour < 12) morningCount++; + else if (hour >= 12 && hour < 18) afternoonCount++; + else if (hour >= 18 && hour < 24) eveningCount++; + else nightCount++; + + dayOfWeekMap[localTime.DayOfWeek] = dayOfWeekMap.GetValueOrDefault(localTime.DayOfWeek, 0) + 1; + } + + // Incorporate repo update timestamps for richer sample size + foreach (var repo in allRepos) + { + var localTime = repo.UpdatedAt.AddHours(2); + int hour = localTime.Hour; + if (hour >= 6 && hour < 12) morningCount++; + else if (hour >= 12 && hour < 18) afternoonCount++; + else if (hour >= 18 && hour < 24) eveningCount++; + else nightCount++; + + dayOfWeekMap[localTime.DayOfWeek] = dayOfWeekMap.GetValueOrDefault(localTime.DayOfWeek, 0) + 1; + } + + int totalEvents = morningCount + afternoonCount + eveningCount + nightCount; + if (totalEvents == 0) totalEvents = 1; + + double morningPct = Math.Round((double)morningCount / totalEvents * 100.0, 1); + double afternoonPct = Math.Round((double)afternoonCount / totalEvents * 100.0, 1); + double eveningPct = Math.Round((double)eveningCount / totalEvents * 100.0, 1); + double nightPct = Math.Round((double)nightCount / totalEvents * 100.0, 1); + + var topDay = dayOfWeekMap.Count > 0 ? dayOfWeekMap.OrderByDescending(kv => kv.Value).First().Key : DayOfWeek.Wednesday; + string topDayName = topDay switch + { + DayOfWeek.Monday => "Montag", + DayOfWeek.Tuesday => "Dienstag", + DayOfWeek.Wednesday => "Mittwoch", + DayOfWeek.Thursday => "Donnerstag", + DayOfWeek.Friday => "Freitag", + DayOfWeek.Saturday => "Samstag", + DayOfWeek.Sunday => "Sonntag", + _ => "Mittwoch" + }; + + string archetype = (nightCount + eveningCount >= morningCount + afternoonCount) + ? "🌙 Night-Owl (Höchste Produktivität am Abend & nachts)" + : "⚡ Day-Runner (Fokus während der regulären Tagesstunden)"; + + var productivity = new ProductivityStats + { + MorningPct = morningPct, + AfternoonPct = afternoonPct, + EveningPct = eveningPct, + NightPct = nightPct, + MorningCount = morningCount, + AfternoonCount = afternoonCount, + EveningCount = eveningCount, + NightCount = nightCount, + MostActiveDayOfWeek = topDayName, + DevArchetype = archetype + }; + + // 7. Build TeamStats TeamStats? teamStats = null; if (teamRepos.Count > 0 || orgs.Count > 0) { @@ -164,7 +232,8 @@ public static class StatsCalculator OverallLanguages = overallLanguages, PersonalRepos = personalRepos, TeamStats = teamStats, - RecentActivities = recentActivities + RecentActivities = recentActivities, + Productivity = productivity }; } diff --git a/src/ProfileManager/Services/SvgGenerator.cs b/src/ProfileManager/Services/SvgGenerator.cs index 7960040..b5889ed 100644 --- a/src/ProfileManager/Services/SvgGenerator.cs +++ b/src/ProfileManager/Services/SvgGenerator.cs @@ -325,6 +325,110 @@ public static class SvgGenerator return sb.ToString(); } + public static string GenerateProductivity(ProductivityStats prod) + { + var sb = new StringBuilder(8192); + + const int svgWidth = 740; + const int svgHeight = 220; + const int gridStartX = 34; + + sb.AppendLine(""""""); + sb.AppendLine($""""""); + + sb.AppendLine(""" + + """); + + // Background + sb.AppendLine($" "); + + // Header + sb.AppendLine(" "); + sb.AppendLine(" ⏱️ Coding Biorhythmus Aktivitäts-Phasen (CET)"); + + string badgeText = prod.DevArchetype; + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" {badgeText}"); + sb.AppendLine(" "); + + // 4 Time Distribution Bars + int barStartY = 60; + int barHeight = 16; + const double maxBarW = 430.0; + + var buckets = new (string Icon, string Label, string Time, double Pct, string Color)[] + { + ("🌅", "Morgen", "06:00 - 12:00", prod.MorningPct, "var(--bar-morning)"), + ("☀️", "Nachmittag", "12:00 - 18:00", prod.AfternoonPct, "var(--bar-afternoon)"), + ("🌆", "Abend", "18:00 - 24:00", prod.EveningPct, "var(--bar-evening)"), + ("🌙", "Nacht", "00:00 - 06:00", prod.NightPct, "var(--bar-night)") + }; + + for (int i = 0; i < buckets.Length; i++) + { + var (icon, label, time, pct, color) = buckets[i]; + int y = barStartY + (i * 36); + + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" {icon} {label}"); + sb.AppendLine($" ({time})"); + + // Progress bar background & fill + int barX = 200; + double w = Math.Max(4.0, (pct / 100.0) * maxBarW); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" {pct:F1}%"); + 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($" ");