feat: add productivity biorhythm clock, developer achievements and quote [skip ci]

This commit is contained in:
NickiCloud 2026-09-02 21:15:59 +02:00
parent 3ba3863f54
commit c4c47ab445
9 changed files with 398 additions and 40 deletions

View File

@ -13,6 +13,7 @@ on:
paths-ignore: paths-ignore:
- 'heatmap.svg' - 'heatmap.svg'
- 'team-stats.svg' - 'team-stats.svg'
- 'productivity.svg'
- 'README.md' - 'README.md'
jobs: jobs:
@ -50,9 +51,9 @@ jobs:
git config --global user.email "bot@webcore-consulting.dev" git config --global user.email "bot@webcore-consulting.dev"
# Prüfen, ob sich SVG-Dateien oder README.md geändert haben # 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..." 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! # GANZ WICHTIG: [skip ci] verhindert Endlosschleifen in Gitea Actions!
git commit -m "chore(profile): update personal & team telemetry [skip ci]" git commit -m "chore(profile): update personal & team telemetry [skip ci]"

View File

@ -12,7 +12,7 @@
<img src="https://img.shields.io/badge/Gitea_Server-Online_%E2%97%8F-2ea44f?style=flat-square&logo=gitea&logoColor=white" alt="Gitea Online" /> <img src="https://img.shields.io/badge/Gitea_Server-Online_%E2%97%8F-2ea44f?style=flat-square&logo=gitea&logoColor=white" alt="Gitea Online" />
<img src="https://img.shields.io/badge/Homelab-Debian_12_Bookworm-A81D33?style=flat-square&logo=debian&logoColor=white" alt="Debian 12" /> <img src="https://img.shields.io/badge/Homelab-Debian_12_Bookworm-A81D33?style=flat-square&logo=debian&logoColor=white" alt="Debian 12" />
<img src="https://img.shields.io/badge/CI%2FCD-Gitea_Act_Runner-609926?style=flat-square&logo=docker&logoColor=white" alt="Runner Active" /> <img src="https://img.shields.io/badge/CI%2FCD-Gitea_Act_Runner-609926?style=flat-square&logo=docker&logoColor=white" alt="Runner Active" />
<img src="https://img.shields.io/badge/Telemetry-Zuletzt_aktualisiert_02.09.2026%2019%3A12_UTC-7e3f98?style=flat-square" alt="Last Sync" /> <img src="https://img.shields.io/badge/Telemetry-Zuletzt_aktualisiert_02.09.2026%2019%3A15_UTC-7e3f98?style=flat-square" alt="Last Sync" />
</p> </p>
<!-- END_SYS_TELEMETRY --> <!-- END_SYS_TELEMETRY -->
@ -115,11 +115,11 @@ Ich bin **Nicki (Nicklas)**, angehender Softwareentwickler mit fundiertem Fokus
> *Echtzeit-Feed der letzten Aktionen und Commits über alle privaten &amp; Team-Repositories.* > *Echtzeit-Feed der letzten Aktionen und Commits über alle privaten &amp; Team-Repositories.*
<!-- START_RECENT_ACTIVITY --> <!-- START_RECENT_ACTIVITY -->
- ⚡ **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* - ⚡ **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*
- ⚡ **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* - ⚡ **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*
- ⚡ **chore(profile): update activity heatmap & stats (22 contributions) [skip ci]** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) • 🕒 *vor 18 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*
- ⚡ **Initial profile setup** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) • 🕒 *vor 19 Minuten* - ⚡ **chore(profile): update activity heatmap & stats (22 contributions) [skip ci]** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) • 🕒 *vor 21 Minuten*
- **Repository neu erstellt** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) • 🕒 *vor 55 Minuten* - **Initial profile setup** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) • 🕒 *vor 22 Minuten*
<!-- END_RECENT_ACTIVITY --> <!-- END_RECENT_ACTIVITY -->
</details> </details>
@ -127,7 +127,37 @@ Ich bin **Nicki (Nicklas)**, angehender Softwareentwickler mit fundiertem Fokus
<br/> <br/>
<details open> <details open>
<summary><h3>🛡️ Tab 4: Homelab, Server-Infrastruktur &amp; Telemetrie</h3></summary> <summary><h3>⏱️ Tab 4: Coding Biorhythmus, Produktivität &amp; Trophäen</h3></summary>
> *Analyse der aktiven Entwicklungsphasen, Trophäen und Entwickler-Weisheiten.*
<div align="center">
<img src="productivity.svg" alt="Nicki's Coding Productivity Clock" width="100%" />
</div>
#### 🏆 Entwickler-Erfolge &amp; Achievements:
<!-- START_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** ✅ |
<!-- END_ACHIEVEMENTS -->
#### 💡 Tägliche Entwickler-Philosophie:
<!-- START_RANDOM_QUOTE -->
> *"Clean code always looks like it was written by someone who cares."*
> — **Robert C. Martin**
<!-- END_RANDOM_QUOTE -->
</details>
<br/>
<details open>
<summary><h3>🛡️ Tab 5: Homelab, Server-Infrastruktur &amp; Telemetrie</h3></summary>
``` ```
┌─────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────┐
@ -158,6 +188,19 @@ Ich bin **Nicki (Nicklas)**, angehender Softwareentwickler mit fundiertem Fokus
</details> </details>
<br/>
<details open>
<summary><h3>🎮 Tab 6: Gaming Battlestation &amp; Loadouts</h3></summary>
| 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 |
</details>
--- ---
## 🛠️ Ausgewählte Eigenprojekte ## 🛠️ Ausgewählte Eigenprojekte
@ -173,12 +216,6 @@ Ich bin **Nicki (Nicklas)**, angehender Softwareentwickler mit fundiertem Fokus
<div align="center"> <div align="center">
### 🎮 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 ```csharp
var nicki = new Developer var nicki = new Developer
{ {

View File

@ -74,7 +74,7 @@
<text class="title" x="0" y="14">🦊 NickiCloud <tspan font-weight="400" fill="var(--accent)"></tspan> <tspan font-weight="500" font-size="12" fill="var(--text-sub)">Persönliche Aktivität</tspan></text> <text class="title" x="0" y="14">🦊 NickiCloud <tspan font-weight="400" fill="var(--accent)"></tspan> <tspan font-weight="500" font-size="12" fill="var(--text-sub)">Persönliche Aktivität</tspan></text>
<rect x="510" y="0" width="190" height="22" rx="11" fill="var(--metric-bg)" stroke="var(--metric-border)" stroke-width="1" /> <rect x="510" y="0" width="190" height="22" rx="11" fill="var(--metric-bg)" stroke="var(--metric-border)" stroke-width="1" />
<circle cx="522" cy="11" r="4" fill="var(--accent)" /> <circle cx="522" cy="11" r="4" fill="var(--accent)" />
<text class="subtitle" x="532" y="15" font-weight="600">25 Commits im letzten Jahr</text> <text class="subtitle" x="532" y="15" font-weight="600">26 Commits im letzten Jahr</text>
</g> </g>
<!-- Month Labels --> <!-- Month Labels -->
<text class="lbl-month" x="34" y="54">Sep</text> <text class="lbl-month" x="34" y="54">Sep</text>
@ -1172,8 +1172,8 @@
<rect class="cell lvl-3" x="697" y="77" width="10" height="10" rx="2" style="animation-delay: 612ms;"> <rect class="cell lvl-3" x="697" y="77" width="10" height="10" rx="2" style="animation-delay: 612ms;">
<title>01.09.2026: 9 Commits</title> <title>01.09.2026: 9 Commits</title>
</rect> </rect>
<rect class="cell lvl-3" x="697" y="90" width="10" height="10" rx="2" style="animation-delay: 612ms;"> <rect class="cell lvl-4" x="697" y="90" width="10" height="10" rx="2" style="animation-delay: 612ms;">
<title>02.09.2026: 9 Commits</title> <title>02.09.2026: 10 Commits</title>
</rect> </rect>
<rect class="cell lvl-0" x="697" y="103" width="10" height="10" rx="2" style="animation-delay: 612ms;"> <rect class="cell lvl-0" x="697" y="103" width="10" height="10" rx="2" style="animation-delay: 612ms;">
<title>03.09.2026: 0 Commits</title> <title>03.09.2026: 0 Commits</title>
@ -1192,7 +1192,7 @@
<g transform="translate(34, 166)"> <g transform="translate(34, 166)">
<rect width="216" height="36" rx="8" fill="var(--metric-bg)" stroke="var(--metric-border)" stroke-width="1" /> <rect width="216" height="36" rx="8" fill="var(--metric-bg)" stroke="var(--metric-border)" stroke-width="1" />
<text x="12" y="27" font-size="16">📦</text> <text x="12" y="27" font-size="16">📦</text>
<text class="stat-val" x="40" y="20">25</text> <text class="stat-val" x="40" y="20">26</text>
<text class="stat-lbl" x="40" y="33">Jahres-Beiträge</text> <text class="stat-lbl" x="40" y="33">Jahres-Beiträge</text>
</g> </g>
<g transform="translate(264, 166)"> <g transform="translate(264, 166)">
@ -1209,30 +1209,30 @@
</g> </g>
<!-- Top Languages Bar --> <!-- Top Languages Bar -->
<g clip-path="url(#langBarClip)"> <g clip-path="url(#langBarClip)">
<rect x="34.0" y="215" width="387.3" height="8" fill="#a855f7"> <rect x="34.0" y="215" width="388.0" height="8" fill="#a855f7">
<title>C#: 57,3%</title> <title>C#: 57,4%</title>
</rect> </rect>
<rect x="421.3" y="215" width="266.3" height="8" fill="#e34c26"> <rect x="422.0" y="215" width="265.0" height="8" fill="#e34c26">
<title>HTML: 39,4%</title> <title>HTML: 39,2%</title>
</rect> </rect>
<rect x="687.7" y="215" width="15.5" height="8" fill="#563d7c"> <rect x="687.0" y="215" width="14.9" height="8" fill="#563d7c">
<title>CSS: 2,3%</title> <title>CSS: 2,2%</title>
</rect> </rect>
<rect x="703.2" y="215" width="2.7" height="8" fill="#f1e05a"> <rect x="701.9" y="215" width="2.7" height="8" fill="#f1e05a">
<title>JavaScript: 0,4%</title> <title>JavaScript: 0,4%</title>
</rect> </rect>
<rect x="705.9" y="215" width="2.7" height="8" fill="#012456"> <rect x="704.6" y="215" width="2.7" height="8" fill="#012456">
<title>PowerShell: 0,4%</title> <title>PowerShell: 0,4%</title>
</rect> </rect>
</g> </g>
<!-- Language Legend --> <!-- Language Legend -->
<g transform="translate(34, 238)"> <g transform="translate(34, 238)">
<circle cx="4" cy="4" r="4" fill="#a855f7" /> <circle cx="4" cy="4" r="4" fill="#a855f7" />
<text class="lang-lbl" x="13" y="7">C# 57,3%</text> <text class="lang-lbl" x="13" y="7">C# 57,4%</text>
<circle cx="89" cy="4" r="4" fill="#e34c26" /> <circle cx="89" cy="4" r="4" fill="#e34c26" />
<text class="lang-lbl" x="98" y="7">HTML 39,4%</text> <text class="lang-lbl" x="98" y="7">HTML 39,2%</text>
<circle cx="188" cy="4" r="4" fill="#563d7c" /> <circle cx="188" cy="4" r="4" fill="#563d7c" />
<text class="lang-lbl" x="197" y="7">CSS 2,3%</text> <text class="lang-lbl" x="197" y="7">CSS 2,2%</text>
<circle cx="273" cy="4" r="4" fill="#f1e05a" /> <circle cx="273" cy="4" r="4" fill="#f1e05a" />
<text class="lang-lbl" x="282" y="7">JavaScript 0,4%</text> <text class="lang-lbl" x="282" y="7">JavaScript 0,4%</text>
<circle cx="407" cy="4" r="4" fill="#012456" /> <circle cx="407" cy="4" r="4" fill="#012456" />

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

83
productivity.svg Normal file
View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 740 220" width="100%" height="100%">
<style>
:root {
--bg: #11111b;
--card: #181825;
--border: #313244;
--text-main: #cdd6f4;
--text-sub: #a6adc8;
--text-muted: #6c7086;
--accent: #d980fa;
--bar-morning: #f59e0b;
--bar-afternoon: #3b82f6;
--bar-evening: #a855f7;
--bar-night: #6366f1;
--metric-bg: #1e1e2e;
--metric-border: #313244;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #fdf4ff;
--card: #ffffff;
--border: #f0abfc;
--text-main: #4a044e;
--text-sub: #701a75;
--text-muted: #a21caf;
--accent: #c026d3;
--bar-morning: #d97706;
--bar-afternoon: #2563eb;
--bar-evening: #9333ea;
--bar-night: #4f46e5;
--metric-bg: #fae8ff;
--metric-border: #f5d0fe;
}
}
.bg-panel { fill: var(--bg); stroke: var(--border); stroke-width: 1.2px; rx: 12px; }
.title { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 700; fill: var(--text-main); }
.subtitle { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 11px; fill: var(--text-sub); }
.stat-val { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 13px; font-weight: 700; fill: var(--text-main); }
.stat-lbl { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 10px; fill: var(--text-sub); }
.bar-lbl { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 11px; font-weight: 600; fill: var(--text-main); }
.bar-time { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 9px; fill: var(--text-muted); }
</style>
<rect class="bg-panel" x="1" y="1" width="738" height="218" />
<g transform="translate(24, 20)">
<text class="title" x="0" y="14">⏱️ Coding Biorhythmus <tspan font-weight="400" fill="var(--accent)"></tspan> <tspan font-weight="500" font-size="12" fill="var(--text-sub)">Aktivitäts-Phasen (CET)</tspan></text>
<rect x="390" y="0" width="310" height="22" rx="11" fill="var(--metric-bg)" stroke="var(--metric-border)" stroke-width="1" />
<circle cx="402" cy="11" r="4" fill="var(--accent)" />
<text class="subtitle" x="412" y="15" font-weight="600">🌙 Night-Owl (Höchste Produktivität am Abend & nachts)</text>
</g>
<!-- Bucket: Morgen -->
<g transform="translate(34, 60)">
<text class="bar-lbl" x="0" y="12">🌅 Morgen</text>
<text class="bar-time" x="110" y="12">(06:00 - 12:00)</text>
<rect x="200" y="0" width="430" height="16" rx="4" fill="var(--metric-bg)" stroke="var(--metric-border)" stroke-width="0.8" />
<rect x="200" y="0" width="4.0" height="16" rx="4" fill="var(--bar-morning)" />
<text class="stat-val" x="644" y="13" font-size="12">0,0%</text>
</g>
<!-- Bucket: Nachmittag -->
<g transform="translate(34, 96)">
<text class="bar-lbl" x="0" y="12">☀️ Nachmittag</text>
<text class="bar-time" x="110" y="12">(12:00 - 18:00)</text>
<rect x="200" y="0" width="430" height="16" rx="4" fill="var(--metric-bg)" stroke="var(--metric-border)" stroke-width="0.8" />
<rect x="200" y="0" width="22.8" height="16" rx="4" fill="var(--bar-afternoon)" />
<text class="stat-val" x="644" y="13" font-size="12">5,3%</text>
</g>
<!-- Bucket: Abend -->
<g transform="translate(34, 132)">
<text class="bar-lbl" x="0" y="12">🌆 Abend</text>
<text class="bar-time" x="110" y="12">(18:00 - 24:00)</text>
<rect x="200" y="0" width="430" height="16" rx="4" fill="var(--metric-bg)" stroke="var(--metric-border)" stroke-width="0.8" />
<rect x="200" y="0" width="294.1" height="16" rx="4" fill="var(--bar-evening)" />
<text class="stat-val" x="644" y="13" font-size="12">68,4%</text>
</g>
<!-- Bucket: Nacht -->
<g transform="translate(34, 168)">
<text class="bar-lbl" x="0" y="12">🌙 Nacht</text>
<text class="bar-time" x="110" y="12">(00:00 - 06:00)</text>
<rect x="200" y="0" width="430" height="16" rx="4" fill="var(--metric-bg)" stroke="var(--metric-border)" stroke-width="0.8" />
<rect x="200" y="0" width="113.1" height="16" rx="4" fill="var(--bar-night)" />
<text class="stat-val" x="644" y="13" font-size="12">26,3%</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -26,6 +26,20 @@ public sealed class DayContribution
public required int Row { get; init; } // 0..6 (0=Mo..6=So) 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 sealed class TeamStats
{ {
public required string OrganizationName { get; init; } public required string OrganizationName { get; init; }
@ -54,4 +68,5 @@ public sealed class ProfileStats
public required List<GiteaRepo> PersonalRepos { get; init; } public required List<GiteaRepo> PersonalRepos { get; init; }
public required TeamStats? TeamStats { get; init; } public required TeamStats? TeamStats { get; init; }
public required List<ActivityItem> RecentActivities { get; init; } public required List<ActivityItem> RecentActivities { get; init; }
public required ProductivityStats Productivity { get; init; }
} }

View File

@ -15,22 +15,27 @@ internal static class Program
==================================================== ====================================================
"""); """);
var sw = System.Diagnostics.Stopwatch.StartNew();
var username = GetConfigValue(args, "--username", "GITEA_USERNAME", "NickiCloud"); 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 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 token = GetConfigValue(args, "--token", "GITEA_TOKEN", string.Empty);
var svgPath = GetConfigValue(args, "--output-svg", "OUTPUT_SVG", "heatmap.svg"); var svgPath = GetConfigValue(args, "--output-svg", "OUTPUT_SVG", "heatmap.svg");
var teamSvgPath = GetConfigValue(args, "--team-svg", "TEAM_SVG", "team-stats.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 readmePath = GetConfigValue(args, "--readme", "README_PATH", "README.md");
var repoRoot = FindRepoRoot(); var repoRoot = FindRepoRoot();
if (!Path.IsPathRooted(svgPath)) svgPath = Path.GetFullPath(Path.Combine(repoRoot, svgPath)); 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(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)); if (!Path.IsPathRooted(readmePath)) readmePath = Path.GetFullPath(Path.Combine(repoRoot, readmePath));
Console.WriteLine($"[Config] Target User : {username}"); Console.WriteLine($"[Config] Target User : {username}");
Console.WriteLine($"[Config] API Endpoint : {apiUrl}"); Console.WriteLine($"[Config] API Endpoint : {apiUrl}");
Console.WriteLine($"[Config] Heatmap SVG : {svgPath}"); Console.WriteLine($"[Config] Heatmap SVG : {svgPath}");
Console.WriteLine($"[Config] Team Stats SVG : {teamSvgPath}"); Console.WriteLine($"[Config] Team Stats SVG : {teamSvgPath}");
Console.WriteLine($"[Config] Productivity SVG : {prodSvgPath}");
Console.WriteLine($"[Config] Target README : {readmePath}"); Console.WriteLine($"[Config] Target README : {readmePath}");
Console.WriteLine(); Console.WriteLine();
@ -94,15 +99,22 @@ internal static class Program
await File.WriteAllTextAsync(teamSvgPath, teamSvg, utf8WithoutBom); 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)) if (File.Exists(readmePath))
{ {
Console.WriteLine($"[README] Injecting personal, team & language metrics into: {readmePath}"); Console.WriteLine($"[README] Injecting personal, team & language metrics into: {readmePath}");
ReadmeInjector.InjectAll(readmePath, stats); ReadmeInjector.InjectAll(readmePath, stats);
} }
sw.Stop();
Console.ForegroundColor = ConsoleColor.Green; 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(); Console.ResetColor();
return 0; return 0;
} }

View File

@ -20,6 +20,12 @@ public static class ReadmeInjector
private const string StartTelemetryMarker = "<!-- START_SYS_TELEMETRY -->"; private const string StartTelemetryMarker = "<!-- START_SYS_TELEMETRY -->";
private const string EndTelemetryMarker = "<!-- END_SYS_TELEMETRY -->"; private const string EndTelemetryMarker = "<!-- END_SYS_TELEMETRY -->";
private const string StartQuoteMarker = "<!-- START_RANDOM_QUOTE -->";
private const string EndQuoteMarker = "<!-- END_RANDOM_QUOTE -->";
private const string StartAchievementsMarker = "<!-- START_ACHIEVEMENTS -->";
private const string EndAchievementsMarker = "<!-- END_ACHIEVEMENTS -->";
public static bool InjectAll(string readmePath, ProfileStats stats) public static bool InjectAll(string readmePath, ProfileStats stats)
{ {
if (!File.Exists(readmePath)) if (!File.Exists(readmePath))
@ -139,9 +145,40 @@ public static class ReadmeInjector
sb.AppendLine($"</p>"); sb.AppendLine($"</p>");
}); });
// 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); var utf8WithoutBom = new UTF8Encoding(false);
File.WriteAllText(readmePath, content, utf8WithoutBom); 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; return true;
} }

View File

@ -133,7 +133,75 @@ public static class StatsCalculator
var overallLanguages = AggregateLanguages(allRepos); var overallLanguages = AggregateLanguages(allRepos);
var teamLanguages = AggregateLanguages(teamRepos); 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<DayOfWeek, int>();
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; TeamStats? teamStats = null;
if (teamRepos.Count > 0 || orgs.Count > 0) if (teamRepos.Count > 0 || orgs.Count > 0)
{ {
@ -164,7 +232,8 @@ public static class StatsCalculator
OverallLanguages = overallLanguages, OverallLanguages = overallLanguages,
PersonalRepos = personalRepos, PersonalRepos = personalRepos,
TeamStats = teamStats, TeamStats = teamStats,
RecentActivities = recentActivities RecentActivities = recentActivities,
Productivity = productivity
}; };
} }

View File

@ -325,6 +325,110 @@ public static class SvgGenerator
return sb.ToString(); 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("""<?xml version="1.0" encoding="utf-8"?>""");
sb.AppendLine($"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {svgWidth} {svgHeight}" width="100%" height="100%">""");
sb.AppendLine("""
<style>
:root {
--bg: #11111b;
--card: #181825;
--border: #313244;
--text-main: #cdd6f4;
--text-sub: #a6adc8;
--text-muted: #6c7086;
--accent: #d980fa;
--bar-morning: #f59e0b;
--bar-afternoon: #3b82f6;
--bar-evening: #a855f7;
--bar-night: #6366f1;
--metric-bg: #1e1e2e;
--metric-border: #313244;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #fdf4ff;
--card: #ffffff;
--border: #f0abfc;
--text-main: #4a044e;
--text-sub: #701a75;
--text-muted: #a21caf;
--accent: #c026d3;
--bar-morning: #d97706;
--bar-afternoon: #2563eb;
--bar-evening: #9333ea;
--bar-night: #4f46e5;
--metric-bg: #fae8ff;
--metric-border: #f5d0fe;
}
}
.bg-panel { fill: var(--bg); stroke: var(--border); stroke-width: 1.2px; rx: 12px; }
.title { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 700; fill: var(--text-main); }
.subtitle { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 11px; fill: var(--text-sub); }
.stat-val { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 13px; font-weight: 700; fill: var(--text-main); }
.stat-lbl { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 10px; fill: var(--text-sub); }
.bar-lbl { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 11px; font-weight: 600; fill: var(--text-main); }
.bar-time { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 9px; fill: var(--text-muted); }
</style>
""");
// Background
sb.AppendLine($" <rect class=\"bg-panel\" x=\"1\" y=\"1\" width=\"{svgWidth - 2}\" height=\"{svgHeight - 2}\" />");
// Header
sb.AppendLine(" <g transform=\"translate(24, 20)\">");
sb.AppendLine(" <text class=\"title\" x=\"0\" y=\"14\">⏱️ Coding Biorhythmus <tspan font-weight=\"400\" fill=\"var(--accent)\">•</tspan> <tspan font-weight=\"500\" font-size=\"12\" fill=\"var(--text-sub)\">Aktivitäts-Phasen (CET)</tspan></text>");
string badgeText = prod.DevArchetype;
sb.AppendLine($" <rect x=\"390\" y=\"0\" width=\"310\" height=\"22\" rx=\"11\" fill=\"var(--metric-bg)\" stroke=\"var(--metric-border)\" stroke-width=\"1\" />");
sb.AppendLine($" <circle cx=\"402\" cy=\"11\" r=\"4\" fill=\"var(--accent)\" />");
sb.AppendLine($" <text class=\"subtitle\" x=\"412\" y=\"15\" font-weight=\"600\">{badgeText}</text>");
sb.AppendLine(" </g>");
// 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($" <!-- Bucket: {label} -->");
sb.AppendLine($" <g transform=\"translate({gridStartX}, {y})\">");
sb.AppendLine($" <text class=\"bar-lbl\" x=\"0\" y=\"12\">{icon} {label}</text>");
sb.AppendLine($" <text class=\"bar-time\" x=\"110\" y=\"12\">({time})</text>");
// Progress bar background & fill
int barX = 200;
double w = Math.Max(4.0, (pct / 100.0) * maxBarW);
sb.AppendLine($" <rect x=\"{barX}\" y=\"0\" width=\"{maxBarW}\" height=\"{barHeight}\" rx=\"4\" fill=\"var(--metric-bg)\" stroke=\"var(--metric-border)\" stroke-width=\"0.8\" />");
sb.AppendLine($" <rect x=\"{barX}\" y=\"0\" width=\"{w.ToString("F1", CultureInfo.InvariantCulture)}\" height=\"{barHeight}\" rx=\"4\" fill=\"{color}\" />");
sb.AppendLine($" <text class=\"stat-val\" x=\"{barX + maxBarW + 14}\" y=\"13\" font-size=\"12\">{pct:F1}%</text>");
sb.AppendLine(" </g>");
}
sb.AppendLine("</svg>");
return sb.ToString();
}
private static void RenderMetricCard(StringBuilder sb, int x, int y, int w, int h, string icon, string value, string label) private static void RenderMetricCard(StringBuilder sb, int x, int y, int w, int h, string icon, string value, string label)
{ {
sb.AppendLine($" <g transform=\"translate({x}, {y})\">"); sb.AppendLine($" <g transform=\"translate({x}, {y})\">");