feat: add team telemetry dashboard, byte-level language analytics and tabs [skip ci]
This commit is contained in:
parent
6f898088f2
commit
06e49cc0fc
@ -1,4 +1,4 @@
|
|||||||
name: Update Gitea Profile
|
name: Update Gitea Profile & Team Telemetry
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
@ -12,6 +12,7 @@ on:
|
|||||||
- main
|
- main
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'heatmap.svg'
|
- 'heatmap.svg'
|
||||||
|
- 'team-stats.svg'
|
||||||
- 'README.md'
|
- 'README.md'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@ -27,7 +28,6 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# Nutzt standardmäßig das automatische Gitea-Token oder ein benutzerdefiniertes Secret
|
|
||||||
token: ${{ secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: ⚙️ .NET 10 SDK einrichten
|
- name: ⚙️ .NET 10 SDK einrichten
|
||||||
@ -49,17 +49,17 @@ jobs:
|
|||||||
git config --global user.name "NickiBot[bot]"
|
git config --global user.name "NickiBot[bot]"
|
||||||
git config --global user.email "bot@webcore-consulting.dev"
|
git config --global user.email "bot@webcore-consulting.dev"
|
||||||
|
|
||||||
# Prüfen, ob sich heatmap.svg 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|README.md)"; then
|
if git status --porcelain | grep -E "(heatmap.svg|team-stats.svg|README.md)"; then
|
||||||
echo "Änderungen an heatmap.svg oder README.md erkannt. Committe..."
|
echo "Änderungen an Grafiken oder README.md erkannt. Committe..."
|
||||||
git add heatmap.svg README.md
|
git add heatmap.svg team-stats.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 activity heatmap & stats [skip ci]"
|
git commit -m "chore(profile): update personal & team telemetry [skip ci]"
|
||||||
|
|
||||||
# Änderungen pushen
|
# Änderungen pushen
|
||||||
git push
|
git push
|
||||||
echo "Profil erfolgreich aktualisiert und gepusht! ✨"
|
echo "Profil & Team-Telemetrie erfolgreich aktualisiert und gepusht! ✨"
|
||||||
else
|
else
|
||||||
echo "Keine Änderungen vorhanden. Alles auf dem neuesten Stand. 👍"
|
echo "Keine Änderungen vorhanden. Alles auf dem neuesten Stand. 👍"
|
||||||
fi
|
fi
|
||||||
|
|||||||
140
README.md
140
README.md
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
# 🦊 Hey, ich bin Nicki!
|
# 🦊 Hey, ich bin Nicki!
|
||||||
|
|
||||||
### 🎓 Fachabiturient & Leidenschaftlicher C# / .NET Softwareentwickler
|
### 🎓 Fachabiturient & Passionierter C# / .NET Software-Architekt
|
||||||
*Building high-performance desktop & web applications with a touch of purple fox magic.*
|
*High-Performance Desktop Apps, moderne Web-Services & kompromissloses Self-Hosting im lila Fuchs-Style.*
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
@ -17,63 +17,130 @@
|
|||||||
[](https://pop.system76.com/)
|
[](https://pop.system76.com/)
|
||||||
[](https://www.debian.org/)
|
[](https://www.debian.org/)
|
||||||
[](https://www.docker.com/)
|
[](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."
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🧙♂️ Ü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**.
|
- 💻 **Fokus:** C#, .NET 10, Cross-Platform Desktop mit **Avalonia UI**, moderne Web-Apps mit **Blazor / MudBlazor** und modulare Microservices.
|
||||||
- 🐧 **Mein OS & Dev-Setup:** Rider-Enthusiast auf **Pop!_OS (Linux)** – weil Entwicklungs-Power Freiheit braucht.
|
- 🏢 **Team & Consulting:** Softwareentwickler bei **WebCore Consulting** – Entwicklung von Management-Tools, Kundenportalen und Security-Services.
|
||||||
- 🏰 **Self-Hosting & Homelab:** Eigener Debian-Server mit Docker-Containern: Gitea, Mailcow, Nginx Reverse-Proxy, abgesichert mit UFW & fail2ban.
|
- 🐧 **Entwicklungsumgebung:** Vollständiger Linux-Workflow mit **JetBrains Rider auf Pop!_OS** – maximale Produktivität ohne Windows-Overhead.
|
||||||
- 🎮 **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**.
|
- 🏰 **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.
|
||||||
- 🦊 **Das Fuchs-Branding:** Lila Akzente, Neugier, Agilität und maximale Eleganz in Code & UI.
|
- 🎮 **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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
## 📊 Meine Gitea Aktivität & Streak
|
## 📊 Live Aktivitäts- & Performance-Dashboards
|
||||||
|
|
||||||
<!-- Die Heatmap wird vollautomatisch per Gitea Action mit .NET 10 generiert -->
|
|
||||||
<img src="heatmap.svg" alt="Nicki's Gitea Contribution Heatmap" width="100%" />
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
<details open>
|
||||||
|
<summary><h3>🦊 Tab 1: Persönliche Aktivität & Heatmap</h3></summary>
|
||||||
|
|
||||||
## 🛠️ 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:
|
<div align="center">
|
||||||
|
<img src="heatmap.svg" alt="Nicki's Gitea Contribution Heatmap" width="100%" />
|
||||||
|
</div>
|
||||||
|
|
||||||
| Projekt | Stack | Beschreibung |
|
#### 📌 Zuletzt aktualisierte persönliche Projekte:
|
||||||
| :--- | :---: | :--- |
|
|
||||||
| 📝 **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
|
|
||||||
|
|
||||||
<!-- Dieser Bereich wird dynamisch von der C# .NET 10 Konsolen-App aktualisiert -->
|
|
||||||
<!-- START_LATEST_REPOS -->
|
<!-- START_LATEST_REPOS -->
|
||||||
- 🚀 [**.profile**](https://git.webcore-consulting.dev/NickiCloud/.profile) – 🦊 Gitea-Profil & dynamische Aktivitäts-Statistiken mit .NET 10
|
- 🚀 [**.profile**](https://git.webcore-consulting.dev/NickiCloud/.profile) – 🦊 Gitea-Profil & dynamische Aktivitäts-Statistiken mit .NET 10
|
||||||
`C#` • 🕒 *Aktualisiert am 02.09.2026*
|
`C#` • 🕒 *Aktualisiert am 02.09.2026*
|
||||||
<!-- END_LATEST_REPOS -->
|
<!-- END_LATEST_REPOS -->
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><h3>🏢 Tab 2: Team- & Organisations-Aktivität (WebCore Consulting)</h3></summary>
|
||||||
|
|
||||||
|
> *Live-Telemetrie aller Team-Repositories, Code-Volumen und Byte-genaue Sprachverteilung.*
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="team-stats.svg" alt="WebCore Consulting Team Statistics" width="100%" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
#### 📈 Byte-genaue Sprachverteilung (Team-Codebase):
|
||||||
|
<!-- START_LANGUAGE_METRICS -->
|
||||||
|
| 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 | `░░░░░░░░░░░░░░░░░░░░` |
|
||||||
|
<!-- END_LANGUAGE_METRICS -->
|
||||||
|
|
||||||
|
#### 🚀 Aktive Team-Repositories:
|
||||||
|
<!-- START_TEAM_REPOS -->
|
||||||
|
- 🏢 [**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*
|
||||||
|
<!-- END_TEAM_REPOS -->
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 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`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
### 🎮 Gaming Corner & Fun Facts
|
### 🎮 Gaming & Fun Corner
|
||||||
|
|
||||||
[](https://minecraft.net)
|
[](https://minecraft.net)
|
||||||
[](https://www.ubisoft.com/game/rainbow-six/siege)
|
[](https://www.ubisoft.com/game/rainbow-six/siege)
|
||||||
@ -82,13 +149,14 @@ Hier sind ein paar Highlights aus meiner Entwickler-Schmiede:
|
|||||||
```csharp
|
```csharp
|
||||||
var nicki = new Developer
|
var nicki = new Developer
|
||||||
{
|
{
|
||||||
Drink = "Kühles Spezi oder Mate",
|
Drink = "Kühles Spezi oder Club-Mate",
|
||||||
Editor = "JetBrains Rider",
|
Workstation = "Pop!_OS on Linux Kernel",
|
||||||
Terminal = "zsh on Pop!_OS",
|
IDE = "JetBrains Rider 2024+",
|
||||||
Motto = "Refactor early, test continuously, deploy autonomously."
|
Team = "WebCore Consulting",
|
||||||
|
Motto = "Measure twice, refactor once, ship autonomously."
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
<sub>Gebaut mit purer C# .NET 10 Power & Gitea Actions • Design im Lila-Fuchs-Style 💜</sub>
|
<sub>Entwickelt mit C# .NET 10 & Gitea Actions • Design im Lila-Fuchs-Style 💜</sub>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
50
heatmap.svg
50
heatmap.svg
@ -71,10 +71,10 @@
|
|||||||
<rect class="bg-panel" x="1" y="1" width="738" height="263" />
|
<rect class="bg-panel" x="1" y="1" width="738" height="263" />
|
||||||
<g transform="translate(20, 20)">
|
<g transform="translate(20, 20)">
|
||||||
<!-- Fox Icon & Title -->
|
<!-- Fox Icon & Title -->
|
||||||
<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)">Gitea Activity</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">23 Commits im letzten Jahr</text>
|
<text class="subtitle" x="532" y="15" font-weight="600">24 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>
|
||||||
@ -1173,7 +1173,7 @@
|
|||||||
<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-3" x="697" y="90" width="10" height="10" rx="2" style="animation-delay: 612ms;">
|
||||||
<title>02.09.2026: 7 Commits</title>
|
<title>02.09.2026: 8 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>
|
||||||
@ -1191,31 +1191,51 @@
|
|||||||
<!-- Stat Badges -->
|
<!-- Stat Badges -->
|
||||||
<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="23" font-size="16">📦</text>
|
<text x="12" y="27" font-size="16">📦</text>
|
||||||
<text class="stat-val" x="38" y="17">23</text>
|
<text class="stat-val" x="40" y="20">24</text>
|
||||||
<text class="stat-lbl" x="38" y="29">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)">
|
||||||
<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="23" font-size="16">🔥</text>
|
<text x="12" y="27" font-size="16">🔥</text>
|
||||||
<text class="stat-val" x="38" y="17">4 Tage</text>
|
<text class="stat-val" x="40" y="20">4 Tage</text>
|
||||||
<text class="stat-lbl" x="38" y="29">Aktueller Streak</text>
|
<text class="stat-lbl" x="40" y="33">Aktueller Streak</text>
|
||||||
</g>
|
</g>
|
||||||
<g transform="translate(494, 166)">
|
<g transform="translate(494, 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="23" font-size="16">⚡</text>
|
<text x="12" y="27" font-size="16">⚡</text>
|
||||||
<text class="stat-val" x="38" y="17">4 Tage</text>
|
<text class="stat-val" x="40" y="20">4 Tage</text>
|
||||||
<text class="stat-lbl" x="38" y="29">Längster Streak</text>
|
<text class="stat-lbl" x="40" y="33">Längster Streak</text>
|
||||||
</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="676.0" height="8" fill="#a855f7">
|
<rect x="34.0" y="215" width="385.3" height="8" fill="#a855f7">
|
||||||
<title>C#: 100,0%</title>
|
<title>C#: 57,0%</title>
|
||||||
|
</rect>
|
||||||
|
<rect x="419.3" y="215" width="268.4" height="8" fill="#e34c26">
|
||||||
|
<title>HTML: 39,7%</title>
|
||||||
|
</rect>
|
||||||
|
<rect x="687.7" y="215" width="15.5" height="8" fill="#563d7c">
|
||||||
|
<title>CSS: 2,3%</title>
|
||||||
|
</rect>
|
||||||
|
<rect x="703.2" y="215" width="2.7" height="8" fill="#f1e05a">
|
||||||
|
<title>JavaScript: 0,4%</title>
|
||||||
|
</rect>
|
||||||
|
<rect x="705.9" y="215" width="2.7" height="8" fill="#012456">
|
||||||
|
<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# 100,0%</text>
|
<text class="lang-lbl" x="13" y="7">C# 57,0%</text>
|
||||||
|
<circle cx="89" cy="4" r="4" fill="#e34c26" />
|
||||||
|
<text class="lang-lbl" x="98" y="7">HTML 39,7%</text>
|
||||||
|
<circle cx="188" cy="4" r="4" fill="#563d7c" />
|
||||||
|
<text class="lang-lbl" x="197" y="7">CSS 2,3%</text>
|
||||||
|
<circle cx="273" cy="4" r="4" fill="#f1e05a" />
|
||||||
|
<text class="lang-lbl" x="282" y="7">JavaScript 0,4%</text>
|
||||||
|
<circle cx="407" cy="4" r="4" fill="#012456" />
|
||||||
|
<text class="lang-lbl" x="416" y="7">PowerShell 0,4%</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
@ -2,14 +2,32 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace ProfileManager.Models;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DTO representing a repository in Gitea's /api/v1/users/{user}/repos response.
|
/// DTO representing a repository in Gitea's /api/v1/repos responses.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GiteaRepo
|
public sealed class GiteaRepo
|
||||||
{
|
{
|
||||||
[JsonPropertyName("id")]
|
[JsonPropertyName("id")]
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("owner")]
|
||||||
|
public RepoOwner? Owner { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("name")]
|
[JsonPropertyName("name")]
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
@ -28,15 +46,30 @@ public sealed class GiteaRepo
|
|||||||
[JsonPropertyName("fork")]
|
[JsonPropertyName("fork")]
|
||||||
public bool Fork { get; set; }
|
public bool Fork { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("private")]
|
||||||
|
public bool Private { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("archived")]
|
[JsonPropertyName("archived")]
|
||||||
public bool Archived { get; set; }
|
public bool Archived { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("size")]
|
||||||
|
public long Size { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("stars_count")]
|
[JsonPropertyName("stars_count")]
|
||||||
public int StarsCount { get; set; }
|
public int StarsCount { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("forks_count")]
|
[JsonPropertyName("forks_count")]
|
||||||
public int ForksCount { get; set; }
|
public int ForksCount { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("open_issues_count")]
|
||||||
|
public int OpenIssuesCount { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("updated_at")]
|
[JsonPropertyName("updated_at")]
|
||||||
public DateTime UpdatedAt { get; set; }
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public Dictionary<string, long> DetailedLanguages { get; set; } = [];
|
||||||
|
|
||||||
|
public bool IsOrganizationRepo(string personalUsername) =>
|
||||||
|
Owner != null && !string.Equals(Owner.Login, personalUsername, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/ProfileManager/Models/Organization.cs
Normal file
30
src/ProfileManager/Models/Organization.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -4,8 +4,17 @@ public sealed class LanguageStat
|
|||||||
{
|
{
|
||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
public required double Percentage { get; init; }
|
public required double Percentage { get; init; }
|
||||||
|
public required long Bytes { get; init; }
|
||||||
public required int RepoCount { get; init; }
|
public required int RepoCount { get; init; }
|
||||||
public required string Color { 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
|
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 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<LanguageStat> Languages { get; init; }
|
||||||
|
public required List<GiteaRepo> 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 sealed class ProfileStats
|
||||||
{
|
{
|
||||||
public required int TotalContributions365 { get; init; }
|
public required int TotalContributions365 { get; init; }
|
||||||
public required int CurrentStreak { get; init; }
|
public required int CurrentStreak { get; init; }
|
||||||
public required int LongestStreak { get; init; }
|
public required int LongestStreak { get; init; }
|
||||||
public required List<DayContribution> Days { get; init; }
|
public required List<DayContribution> Days { get; init; }
|
||||||
public required List<LanguageStat> TopLanguages { get; init; }
|
public required List<LanguageStat> OverallLanguages { get; init; }
|
||||||
public required List<GiteaRepo> LatestRepos { get; init; }
|
public required List<GiteaRepo> PersonalRepos { get; init; }
|
||||||
|
public required TeamStats? TeamStats { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,85 +11,96 @@ internal static class Program
|
|||||||
Console.WriteLine("""
|
Console.WriteLine("""
|
||||||
====================================================
|
====================================================
|
||||||
🦊 NickiCloud Gitea Profile Manager (.NET 10)
|
🦊 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 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 readmePath = GetConfigValue(args, "--readme", "README_PATH", "README.md");
|
var readmePath = GetConfigValue(args, "--readme", "README_PATH", "README.md");
|
||||||
|
|
||||||
// Resolve paths relative to working directory or repo root
|
var repoRoot = FindRepoRoot();
|
||||||
if (!Path.IsPathRooted(svgPath))
|
if (!Path.IsPathRooted(svgPath)) svgPath = Path.GetFullPath(Path.Combine(repoRoot, svgPath));
|
||||||
{
|
if (!Path.IsPathRooted(teamSvgPath)) teamSvgPath = Path.GetFullPath(Path.Combine(repoRoot, teamSvgPath));
|
||||||
svgPath = Path.GetFullPath(Path.Combine(FindRepoRoot(), svgPath));
|
if (!Path.IsPathRooted(readmePath)) readmePath = Path.GetFullPath(Path.Combine(repoRoot, readmePath));
|
||||||
}
|
|
||||||
|
|
||||||
if (!Path.IsPathRooted(readmePath))
|
Console.WriteLine($"[Config] Target User : {username}");
|
||||||
{
|
Console.WriteLine($"[Config] API Endpoint : {apiUrl}");
|
||||||
readmePath = Path.GetFullPath(Path.Combine(FindRepoRoot(), readmePath));
|
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] Output SVG : {svgPath}");
|
|
||||||
Console.WriteLine($"[Config] Target README: {readmePath}");
|
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var client = new GiteaApiClient(apiUrl, string.IsNullOrWhiteSpace(token) ? null : token);
|
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 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 heatmap = await heatmapTask;
|
||||||
var repos = await reposTask;
|
var repos = await reposTask;
|
||||||
|
var orgs = await orgsTask;
|
||||||
|
|
||||||
// Step 2: Calculate metrics
|
// Step 2: Fetch detailed language byte counts per repository
|
||||||
Console.WriteLine("[Stats] Calculating streaks, contributions and language metrics...");
|
Console.WriteLine("[API] Fetching byte-level language breakdown for all repositories...");
|
||||||
var stats = StatsCalculator.Calculate(heatmap, repos);
|
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}");
|
// Step 3: Calculate multi-tier statistics
|
||||||
Console.WriteLine($"[Stats] Current Streak : {stats.CurrentStreak} days");
|
Console.WriteLine("[Stats] Calculating Personal, Team & Byte metrics...");
|
||||||
Console.WriteLine($"[Stats] Longest Streak : {stats.LongestStreak} days");
|
var stats = StatsCalculator.Calculate(heatmap, repos, orgs, username);
|
||||||
Console.WriteLine($"[Stats] Top Languages : {string.Join(", ", stats.TopLanguages.Select(l => $"{l.Name} ({l.Percentage:F1}%)"))}");
|
|
||||||
Console.WriteLine($"[Stats] Latest Repositories : {string.Join(", ", stats.LatestRepos.Select(r => r.Name))}");
|
Console.WriteLine($"[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();
|
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);
|
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))
|
if (File.Exists(readmePath))
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[README] Injecting latest repositories into: {readmePath}");
|
Console.WriteLine($"[README] Injecting personal, team & language metrics into: {readmePath}");
|
||||||
ReadmeInjector.InjectLatestRepos(readmePath, stats.LatestRepos);
|
ReadmeInjector.InjectAll(readmePath, stats);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
|
||||||
Console.WriteLine($"[README-WARN] Target README not found at {readmePath}. Skipping injection.");
|
|
||||||
Console.ResetColor();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.ForegroundColor = ConsoleColor.Green;
|
Console.ForegroundColor = ConsoleColor.Green;
|
||||||
Console.WriteLine("\n✨ Profile update finished successfully!");
|
Console.WriteLine("\n✨ Multi-tier profile update completed successfully!");
|
||||||
Console.ResetColor();
|
Console.ResetColor();
|
||||||
return 0;
|
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)
|
private static string GetConfigValue(string[] args, string flag, string envVar, string defaultValue)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < args.Length - 1; i++)
|
for (int i = 0; i < args.Length - 1; i++)
|
||||||
|
|||||||
@ -8,6 +8,7 @@ public sealed class GiteaApiClient
|
|||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly string _baseUrl;
|
private readonly string _baseUrl;
|
||||||
|
private readonly bool _hasToken;
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = true
|
PropertyNameCaseInsensitive = true
|
||||||
@ -29,12 +30,13 @@ public sealed class GiteaApiClient
|
|||||||
Timeout = TimeSpan.FromSeconds(15)
|
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"));
|
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
|
||||||
var token = apiToken ?? Environment.GetEnvironmentVariable("GITEA_TOKEN");
|
var token = apiToken ?? Environment.GetEnvironmentVariable("GITEA_TOKEN");
|
||||||
if (!string.IsNullOrWhiteSpace(token))
|
if (!string.IsNullOrWhiteSpace(token))
|
||||||
{
|
{
|
||||||
|
_hasToken = true;
|
||||||
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,28 +55,31 @@ public sealed class GiteaApiClient
|
|||||||
var items = JsonSerializer.Deserialize<List<HeatmapItem>>(json, JsonOptions);
|
var items = JsonSerializer.Deserialize<List<HeatmapItem>>(json, JsonOptions);
|
||||||
if (items != null && items.Count > 0)
|
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;
|
return items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
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();
|
Console.ResetColor();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
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();
|
Console.ResetColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
return GenerateMockHeatmap();
|
return GenerateMockHeatmap();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<GiteaRepo>> GetReposAsync(string username, CancellationToken cancellationToken = default)
|
public async Task<List<GiteaRepo>> 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}");
|
Console.WriteLine($"[API] Fetching repositories from: {url}");
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -86,45 +91,109 @@ public sealed class GiteaApiClient
|
|||||||
var repos = JsonSerializer.Deserialize<List<GiteaRepo>>(json, JsonOptions);
|
var repos = JsonSerializer.Deserialize<List<GiteaRepo>>(json, JsonOptions);
|
||||||
if (repos != null && repos.Count > 0)
|
if (repos != null && repos.Count > 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[API] Successfully retrieved {repos.Count} repositories.");
|
Console.WriteLine($"[API] Retrieved {repos.Count} total repositories.");
|
||||||
return repos;
|
return repos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
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();
|
Console.ResetColor();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
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();
|
Console.ResetColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
return GenerateMockRepos();
|
return GenerateMockRepos();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<GiteaOrganization>> 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<List<GiteaOrganization>>(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<Dictionary<string, long>> 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<Dictionary<string, long>>(json, JsonOptions);
|
||||||
|
if (langs != null)
|
||||||
|
{
|
||||||
|
return langs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore single repo language errors and return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
private static List<HeatmapItem> GenerateMockHeatmap()
|
private static List<HeatmapItem> GenerateMockHeatmap()
|
||||||
{
|
{
|
||||||
var items = new List<HeatmapItem>();
|
var items = new List<HeatmapItem>();
|
||||||
var now = DateTimeOffset.UtcNow;
|
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++)
|
for (int i = 0; i < 365; i++)
|
||||||
{
|
{
|
||||||
var day = now.AddDays(-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;
|
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)
|
if (random.Next(100) < chance)
|
||||||
{
|
{
|
||||||
int commits = random.Next(1, 14);
|
|
||||||
items.Add(new HeatmapItem
|
items.Add(new HeatmapItem
|
||||||
{
|
{
|
||||||
Timestamp = day.ToUnixTimeSeconds(),
|
Timestamp = day.ToUnixTimeSeconds(),
|
||||||
Contributions = commits
|
Contributions = random.Next(1, 12)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,69 +208,47 @@ public sealed class GiteaApiClient
|
|||||||
[
|
[
|
||||||
new GiteaRepo
|
new GiteaRepo
|
||||||
{
|
{
|
||||||
Name = "Markdown-Editor-Pro",
|
Name = "USBSafe",
|
||||||
FullName = "NickiCloud/Markdown-Editor-Pro",
|
FullName = "WebCore/USBSafe",
|
||||||
Description = "Moderner, performanter Markdown-Editor mit Live-Vorschau, Syntax-Highlighting & LaTeX-Support auf Avalonia UI.",
|
Description = "Sicherheits- & Verschlüsselungs-Tool für Wechseldatenträger im Unternehmensnetzwerk.",
|
||||||
HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/Markdown-Editor-Pro",
|
HtmlUrl = "https://git.webcore-consulting.dev/WebCore/USBSafe",
|
||||||
Language = "C#",
|
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,
|
StarsCount = 2,
|
||||||
ForksCount = 0,
|
UpdatedAt = now.AddHours(-1),
|
||||||
UpdatedAt = now.AddDays(-25)
|
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ß" }
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,20 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using ProfileManager.Models;
|
using ProfileManager.Models;
|
||||||
|
|
||||||
namespace ProfileManager.Services;
|
namespace ProfileManager.Services;
|
||||||
|
|
||||||
public static class ReadmeInjector
|
public static class ReadmeInjector
|
||||||
{
|
{
|
||||||
private const string StartMarker = "<!-- START_LATEST_REPOS -->";
|
private const string StartReposMarker = "<!-- START_LATEST_REPOS -->";
|
||||||
private const string EndMarker = "<!-- END_LATEST_REPOS -->";
|
private const string EndReposMarker = "<!-- END_LATEST_REPOS -->";
|
||||||
|
|
||||||
public static bool InjectLatestRepos(string readmePath, List<GiteaRepo> repos)
|
private const string StartTeamReposMarker = "<!-- START_TEAM_REPOS -->";
|
||||||
|
private const string EndTeamReposMarker = "<!-- END_TEAM_REPOS -->";
|
||||||
|
|
||||||
|
private const string StartLangMarker = "<!-- START_LANGUAGE_METRICS -->";
|
||||||
|
private const string EndLangMarker = "<!-- END_LANGUAGE_METRICS -->";
|
||||||
|
|
||||||
|
public static bool InjectAll(string readmePath, ProfileStats stats)
|
||||||
{
|
{
|
||||||
if (!File.Exists(readmePath))
|
if (!File.Exists(readmePath))
|
||||||
{
|
{
|
||||||
@ -20,52 +25,97 @@ public static class ReadmeInjector
|
|||||||
}
|
}
|
||||||
|
|
||||||
var content = File.ReadAllText(readmePath, Encoding.UTF8);
|
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<StringBuilder> builderAction)
|
||||||
|
{
|
||||||
|
int startIndex = source.IndexOf(startMarker, StringComparison.Ordinal);
|
||||||
|
int endIndex = source.IndexOf(endMarker, StringComparison.Ordinal);
|
||||||
|
|
||||||
if (startIndex == -1 || endIndex == -1 || endIndex < startIndex)
|
if (startIndex == -1 || endIndex == -1 || endIndex < startIndex)
|
||||||
{
|
{
|
||||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
return source;
|
||||||
Console.WriteLine($"[README-WARN] Markers '{StartMarker}' and '{EndMarker}' not found in {readmePath}");
|
|
||||||
Console.ResetColor();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var replacement = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
replacement.AppendLine(StartMarker);
|
sb.AppendLine(startMarker);
|
||||||
|
builderAction(sb);
|
||||||
|
sb.Append(endMarker);
|
||||||
|
|
||||||
if (repos.Count == 0)
|
return source.Substring(0, startIndex) + sb + source.Substring(endIndex + endMarker.Length);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,33 +4,34 @@ namespace ProfileManager.Services;
|
|||||||
|
|
||||||
public static class StatsCalculator
|
public static class StatsCalculator
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<string, string> LanguageColors = new(StringComparer.OrdinalIgnoreCase)
|
public static readonly Dictionary<string, string> LanguageColors = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ "C#", "#a855f7" },
|
{ "C#", "#a855f7" },
|
||||||
{ "C++", "#f34b7d" },
|
|
||||||
{ "C", "#555555" },
|
|
||||||
{ "HTML", "#e34c26" },
|
{ "HTML", "#e34c26" },
|
||||||
{ "CSS", "#563d7c" },
|
{ "CSS", "#563d7c" },
|
||||||
{ "JavaScript", "#f1e05a" },
|
{ "JavaScript", "#f1e05a" },
|
||||||
{ "TypeScript", "#3178c6" },
|
{ "TypeScript", "#3178c6" },
|
||||||
{ "Python", "#3572A5" },
|
{ "PowerShell", "#012456" },
|
||||||
{ "Shell", "#89e051" },
|
{ "Shell", "#89e051" },
|
||||||
{ "Bash", "#89e051" },
|
{ "Bash", "#89e051" },
|
||||||
{ "Dockerfile", "#384d54" },
|
{ "Dockerfile", "#384d54" },
|
||||||
|
{ "Python", "#3572A5" },
|
||||||
{ "Go", "#00ADD8" },
|
{ "Go", "#00ADD8" },
|
||||||
{ "Rust", "#dea584" },
|
{ "Rust", "#dea584" },
|
||||||
{ "Vue", "#41b883" },
|
{ "Vue", "#41b883" },
|
||||||
{ "PHP", "#4F5D95" },
|
{ "XAML", "#b53d9e" }
|
||||||
{ "Kotlin", "#A97BFF" },
|
|
||||||
{ "Java", "#b07219" },
|
|
||||||
{ "Dart", "#00B4AB" }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static ProfileStats Calculate(List<HeatmapItem> heatmapItems, List<GiteaRepo> repos, DateOnly? referenceDate = null)
|
public static ProfileStats Calculate(
|
||||||
|
List<HeatmapItem> heatmapItems,
|
||||||
|
List<GiteaRepo> allRepos,
|
||||||
|
List<GiteaOrganization> orgs,
|
||||||
|
string personalUsername = "NickiCloud",
|
||||||
|
DateOnly? referenceDate = null)
|
||||||
{
|
{
|
||||||
var today = referenceDate ?? DateOnly.FromDateTime(DateTime.UtcNow);
|
var today = referenceDate ?? DateOnly.FromDateTime(DateTime.UtcNow);
|
||||||
|
|
||||||
// 1. Group heatmap contributions by DateOnly (UTC)
|
// 1. Heatmap Contributions Map
|
||||||
var dateMap = new Dictionary<DateOnly, int>();
|
var dateMap = new Dictionary<DateOnly, int>();
|
||||||
foreach (var item in heatmapItems)
|
foreach (var item in heatmapItems)
|
||||||
{
|
{
|
||||||
@ -38,7 +39,7 @@ public static class StatsCalculator
|
|||||||
dateMap[date] = dateMap.GetValueOrDefault(date, 0) + Math.Max(0, item.Contributions);
|
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;
|
int dayOffsetFromMonday = ((int)today.DayOfWeek + 6) % 7;
|
||||||
var gridEndSunday = today.AddDays(6 - dayOffsetFromMonday);
|
var gridEndSunday = today.AddDays(6 - dayOffsetFromMonday);
|
||||||
var gridStartMonday = gridEndSunday.AddDays(-(52 * 7 - 1));
|
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);
|
var oneYearAgo = today.AddDays(-365);
|
||||||
int total365 = dateMap
|
int total365 = dateMap.Where(kv => kv.Key >= oneYearAgo && kv.Key <= today).Sum(kv => kv.Value);
|
||||||
.Where(kv => kv.Key >= oneYearAgo && kv.Key <= today)
|
|
||||||
.Sum(kv => kv.Value);
|
|
||||||
|
|
||||||
// 4. Streaks calculation
|
|
||||||
int currentStreak = 0;
|
int currentStreak = 0;
|
||||||
if (dateMap.GetValueOrDefault(today, 0) > 0)
|
if (dateMap.GetValueOrDefault(today, 0) > 0)
|
||||||
{
|
{
|
||||||
@ -90,7 +88,6 @@ public static class StatsCalculator
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// If today has no commit yet, yesterday's streak is still active
|
|
||||||
var check = today.AddDays(-1);
|
var check = today.AddDays(-1);
|
||||||
while (dateMap.GetValueOrDefault(check, 0) > 0)
|
while (dateMap.GetValueOrDefault(check, 0) > 0)
|
||||||
{
|
{
|
||||||
@ -99,7 +96,6 @@ public static class StatsCalculator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Longest Streak across all dates
|
|
||||||
int longestStreak = 0;
|
int longestStreak = 0;
|
||||||
if (dateMap.Count > 0)
|
if (dateMap.Count > 0)
|
||||||
{
|
{
|
||||||
@ -121,50 +117,41 @@ public static class StatsCalculator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Top 3 Repos (sorted by UpdatedAt desc, exclude forks)
|
// 4. Split Personal vs. Team Repos
|
||||||
var nonForkRepos = repos.Where(r => !r.Fork).ToList();
|
var personalRepos = allRepos
|
||||||
var candidateRepos = nonForkRepos.Count > 0 ? nonForkRepos : repos;
|
.Where(r => !r.IsOrganizationRepo(personalUsername))
|
||||||
|
|
||||||
var latestRepos = candidateRepos
|
|
||||||
.OrderByDescending(r => r.UpdatedAt)
|
.OrderByDescending(r => r.UpdatedAt)
|
||||||
.Take(3)
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// 6. Top Languages calculation
|
var teamRepos = allRepos
|
||||||
var languageGroups = candidateRepos
|
.Where(r => r.IsOrganizationRepo(personalUsername))
|
||||||
.Where(r => !string.IsNullOrWhiteSpace(r.Language))
|
.OrderByDescending(r => r.UpdatedAt)
|
||||||
.GroupBy(r => r.Language!.Trim())
|
|
||||||
.Select(g => new { Name = g.Key, Count = g.Count() })
|
|
||||||
.OrderByDescending(x => x.Count)
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
int totalLanguageRepos = languageGroups.Sum(x => x.Count);
|
// 5. Calculate Languages (from Byte Breakdown)
|
||||||
var topLanguages = new List<LanguageStat>();
|
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);
|
OrganizationName = orgName,
|
||||||
topLanguages.Add(new LanguageStat
|
OrganizationLogin = orgLogin,
|
||||||
{
|
Website = website,
|
||||||
Name = lang.Name,
|
TotalRepos = teamRepos.Count,
|
||||||
Percentage = pct,
|
TotalCodeBytes = totalBytes,
|
||||||
RepoCount = lang.Count,
|
Languages = teamLanguages,
|
||||||
Color = LanguageColors.GetValueOrDefault(lang.Name, "#c084fc")
|
Repos = teamRepos
|
||||||
});
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Default language representation if repos have no language tags
|
|
||||||
topLanguages.Add(new LanguageStat
|
|
||||||
{
|
|
||||||
Name = "C#",
|
|
||||||
Percentage = 100.0,
|
|
||||||
RepoCount = 1,
|
|
||||||
Color = "#a855f7"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ProfileStats
|
return new ProfileStats
|
||||||
@ -173,8 +160,65 @@ public static class StatsCalculator
|
|||||||
CurrentStreak = currentStreak,
|
CurrentStreak = currentStreak,
|
||||||
LongestStreak = Math.Max(longestStreak, currentStreak),
|
LongestStreak = Math.Max(longestStreak, currentStreak),
|
||||||
Days = days,
|
Days = days,
|
||||||
TopLanguages = topLanguages,
|
OverallLanguages = overallLanguages,
|
||||||
LatestRepos = latestRepos
|
PersonalRepos = personalRepos,
|
||||||
|
TeamStats = teamStats
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<LanguageStat> AggregateLanguages(List<GiteaRepo> repos)
|
||||||
|
{
|
||||||
|
var byteMap = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var countMap = new Dictionary<string, int>(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<LanguageStat>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ public static class SvgGenerator
|
|||||||
"Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"
|
"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);
|
var sb = new StringBuilder(16384);
|
||||||
|
|
||||||
@ -104,9 +104,8 @@ public static class SvgGenerator
|
|||||||
// Header: Title & Total Commits Badge
|
// Header: Title & Total Commits Badge
|
||||||
sb.AppendLine(" <g transform=\"translate(20, 20)\">");
|
sb.AppendLine(" <g transform=\"translate(20, 20)\">");
|
||||||
sb.AppendLine(" <!-- Fox Icon & Title -->");
|
sb.AppendLine(" <!-- Fox Icon & Title -->");
|
||||||
sb.AppendLine(" <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)\">Gitea Activity</tspan></text>");
|
sb.AppendLine(" <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>");
|
||||||
|
|
||||||
// Header badge (right aligned)
|
|
||||||
string badgeText = $"{stats.TotalContributions365:N0} Commits im letzten Jahr";
|
string badgeText = $"{stats.TotalContributions365:N0} Commits im letzten Jahr";
|
||||||
sb.AppendLine($" <rect x=\"510\" y=\"0\" width=\"190\" height=\"22\" rx=\"11\" fill=\"var(--metric-bg)\" stroke=\"var(--metric-border)\" stroke-width=\"1\" />");
|
sb.AppendLine($" <rect x=\"510\" y=\"0\" width=\"190\" height=\"22\" rx=\"11\" fill=\"var(--metric-bg)\" stroke=\"var(--metric-border)\" stroke-width=\"1\" />");
|
||||||
sb.AppendLine($" <circle cx=\"522\" cy=\"11\" r=\"4\" fill=\"var(--accent)\" />");
|
sb.AppendLine($" <circle cx=\"522\" cy=\"11\" r=\"4\" fill=\"var(--accent)\" />");
|
||||||
@ -122,7 +121,6 @@ public static class SvgGenerator
|
|||||||
if (firstDayOfCol != null && firstDayOfCol.Date.Month != lastMonth)
|
if (firstDayOfCol != null && firstDayOfCol.Date.Month != lastMonth)
|
||||||
{
|
{
|
||||||
lastMonth = firstDayOfCol.Date.Month;
|
lastMonth = firstDayOfCol.Date.Month;
|
||||||
// Avoid rendering month if too close to right edge
|
|
||||||
if (col < 50)
|
if (col < 50)
|
||||||
{
|
{
|
||||||
int x = gridStartX + col * colStep;
|
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(" <!-- Weekday Labels -->");
|
sb.AppendLine(" <!-- Weekday Labels -->");
|
||||||
sb.AppendLine($" <text class=\"lbl-day\" x=\"15\" y=\"{gridStartY + 8}\">Mo</text>");
|
sb.AppendLine($" <text class=\"lbl-day\" x=\"15\" y=\"{gridStartY + 8}\">Mo</text>");
|
||||||
sb.AppendLine($" <text class=\"lbl-day\" x=\"15\" y=\"{gridStartY + 2 * colStep + 8}\">Mi</text>");
|
sb.AppendLine($" <text class=\"lbl-day\" x=\"15\" y=\"{gridStartY + 2 * colStep + 8}\">Mi</text>");
|
||||||
@ -160,13 +158,8 @@ public static class SvgGenerator
|
|||||||
int cardWidth = 216;
|
int cardWidth = 216;
|
||||||
int gap = 14;
|
int gap = 14;
|
||||||
|
|
||||||
// Card 1: Total
|
|
||||||
RenderMetricCard(sb, gridStartX, cardY, cardWidth, cardHeight, "📦", $"{stats.TotalContributions365:N0}", "Jahres-Beiträge");
|
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");
|
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");
|
RenderMetricCard(sb, gridStartX + (cardWidth + gap) * 2, cardY, cardWidth, cardHeight, "⚡", $"{stats.LongestStreak} {(stats.LongestStreak == 1 ? "Tag" : "Tage")}", "Längster Streak");
|
||||||
|
|
||||||
// Top Languages Bar (width = 676px)
|
// Top Languages Bar (width = 676px)
|
||||||
@ -175,7 +168,7 @@ public static class SvgGenerator
|
|||||||
double currentX = gridStartX;
|
double currentX = gridStartX;
|
||||||
const double totalBarWidth = 676.0;
|
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);
|
double w = Math.Max(2.0, (lang.Percentage / 100.0) * totalBarWidth);
|
||||||
sb.AppendLine($" <rect x=\"{currentX.ToString("F1", CultureInfo.InvariantCulture)}\" y=\"215\" width=\"{w.ToString("F1", CultureInfo.InvariantCulture)}\" height=\"8\" fill=\"{lang.Color}\">");
|
sb.AppendLine($" <rect x=\"{currentX.ToString("F1", CultureInfo.InvariantCulture)}\" y=\"215\" width=\"{w.ToString("F1", CultureInfo.InvariantCulture)}\" height=\"8\" fill=\"{lang.Color}\">");
|
||||||
@ -189,7 +182,7 @@ public static class SvgGenerator
|
|||||||
sb.AppendLine(" <!-- Language Legend -->");
|
sb.AppendLine(" <!-- Language Legend -->");
|
||||||
sb.AppendLine(" <g transform=\"translate(34, 238)\">");
|
sb.AppendLine(" <g transform=\"translate(34, 238)\">");
|
||||||
int legendX = 0;
|
int legendX = 0;
|
||||||
foreach (var lang in stats.TopLanguages)
|
foreach (var lang in stats.OverallLanguages.Take(5))
|
||||||
{
|
{
|
||||||
sb.AppendLine($" <circle cx=\"{legendX + 4}\" cy=\"4\" r=\"4\" fill=\"{lang.Color}\" />");
|
sb.AppendLine($" <circle cx=\"{legendX + 4}\" cy=\"4\" r=\"4\" fill=\"{lang.Color}\" />");
|
||||||
string text = $"{lang.Name} {lang.Percentage:F1}%";
|
string text = $"{lang.Name} {lang.Percentage:F1}%";
|
||||||
@ -202,13 +195,143 @@ public static class SvgGenerator
|
|||||||
return sb.ToString();
|
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("""<?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(" <defs>");
|
||||||
|
sb.AppendLine(" <clipPath id=\"teamLangBarClip\">");
|
||||||
|
sb.AppendLine($" <rect x=\"{gridStartX}\" y=\"125\" width=\"{totalBarWidth}\" height=\"12\" rx=\"6\" />");
|
||||||
|
sb.AppendLine(" </clipPath>");
|
||||||
|
sb.AppendLine(" </defs>");
|
||||||
|
|
||||||
|
sb.AppendLine("""
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #11111b;
|
||||||
|
--card: #181825;
|
||||||
|
--border: #313244;
|
||||||
|
--text-main: #cdd6f4;
|
||||||
|
--text-sub: #a6adc8;
|
||||||
|
--text-muted: #6c7086;
|
||||||
|
--accent: #89b4fa;
|
||||||
|
--metric-bg: #1e1e2e;
|
||||||
|
--metric-border: #313244;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--bg: #f8fafc;
|
||||||
|
--card: #ffffff;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
--text-main: #0f172a;
|
||||||
|
--text-sub: #334155;
|
||||||
|
--text-muted: #64748b;
|
||||||
|
--accent: #2563eb;
|
||||||
|
--metric-bg: #f1f5f9;
|
||||||
|
--metric-border: #cbd5e1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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); }
|
||||||
|
.lang-lbl { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 10px; font-weight: 500; fill: var(--text-sub); }
|
||||||
|
.project-tag { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 10px; fill: var(--text-muted); }
|
||||||
|
</style>
|
||||||
|
""");
|
||||||
|
|
||||||
|
// Background Panel
|
||||||
|
sb.AppendLine($" <rect class=\"bg-panel\" x=\"1\" y=\"1\" width=\"{svgWidth - 2}\" height=\"{svgHeight - 2}\" />");
|
||||||
|
|
||||||
|
// Header: Team Title & Badge
|
||||||
|
sb.AppendLine(" <g transform=\"translate(24, 20)\">");
|
||||||
|
sb.AppendLine($" <text class=\"title\" x=\"0\" y=\"14\">🏢 {team.OrganizationName} <tspan font-weight=\"400\" fill=\"var(--accent)\">•</tspan> <tspan font-weight=\"500\" font-size=\"12\" fill=\"var(--text-sub)\">Team & Projekt-Statistiken</tspan></text>");
|
||||||
|
|
||||||
|
string badgeText = $"{team.TotalRepos} Team-Repositories";
|
||||||
|
sb.AppendLine($" <rect x=\"500\" y=\"0\" width=\"185\" height=\"22\" rx=\"11\" fill=\"var(--metric-bg)\" stroke=\"var(--metric-border)\" stroke-width=\"1\" />");
|
||||||
|
sb.AppendLine($" <circle cx=\"512\" cy=\"11\" r=\"4\" fill=\"#a855f7\" />");
|
||||||
|
sb.AppendLine($" <text class=\"subtitle\" x=\"522\" y=\"15\" font-weight=\"600\">{badgeText}</text>");
|
||||||
|
sb.AppendLine(" </g>");
|
||||||
|
|
||||||
|
// 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(" <!-- Team Languages Progress Bar -->");
|
||||||
|
sb.AppendLine(" <g clip-path=\"url(#teamLangBarClip)\">");
|
||||||
|
double currentX = gridStartX;
|
||||||
|
foreach (var lang in team.Languages)
|
||||||
|
{
|
||||||
|
double w = Math.Max(3.0, (lang.Percentage / 100.0) * totalBarWidth);
|
||||||
|
sb.AppendLine($" <rect x=\"{currentX.ToString("F1", CultureInfo.InvariantCulture)}\" y=\"125\" width=\"{w.ToString("F1", CultureInfo.InvariantCulture)}\" height=\"12\" fill=\"{lang.Color}\">");
|
||||||
|
sb.AppendLine($" <title>{lang.Name}: {lang.Percentage:F1}% ({lang.FormattedSize})</title>");
|
||||||
|
sb.AppendLine(" </rect>");
|
||||||
|
currentX += w;
|
||||||
|
}
|
||||||
|
sb.AppendLine(" </g>");
|
||||||
|
|
||||||
|
// Team Languages Legend (Detailed with Byte Sizes)
|
||||||
|
sb.AppendLine(" <!-- Detailed Language Legend -->");
|
||||||
|
sb.AppendLine(" <g transform=\"translate(34, 158)\">");
|
||||||
|
int legendX = 0;
|
||||||
|
int legendY = 0;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
foreach (var lang in team.Languages.Take(6))
|
||||||
|
{
|
||||||
|
sb.AppendLine($" <circle cx=\"{legendX + 4}\" cy=\"{legendY + 4}\" r=\"4\" fill=\"{lang.Color}\" />");
|
||||||
|
string text = $"{lang.Name}: {lang.Percentage:F1}% ({lang.FormattedSize})";
|
||||||
|
sb.AppendLine($" <text class=\"lang-lbl\" x=\"{legendX + 13}\" y=\"{legendY + 7}\">{text}</text>");
|
||||||
|
|
||||||
|
count++;
|
||||||
|
if (count % 3 == 0)
|
||||||
|
{
|
||||||
|
legendX = 0;
|
||||||
|
legendY += 18;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
legendX += 225;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.AppendLine(" </g>");
|
||||||
|
|
||||||
|
// Bottom active repos pills
|
||||||
|
sb.AppendLine(" <!-- Active Team Repositories Footer -->");
|
||||||
|
sb.AppendLine(" <g transform=\"translate(34, 215)\">");
|
||||||
|
string repoPills = string.Join(" • ", team.Repos.Take(3).Select(r => $"🚀 {r.Name} ({r.Language ?? "Code"})"));
|
||||||
|
sb.AppendLine($" <text class=\"project-tag\" x=\"0\" y=\"0\">Team-Repositories: {repoPills}</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})\">");
|
||||||
sb.AppendLine($" <rect width=\"{w}\" height=\"{h}\" rx=\"8\" fill=\"var(--metric-bg)\" stroke=\"var(--metric-border)\" stroke-width=\"1\" />");
|
sb.AppendLine($" <rect width=\"{w}\" height=\"{h}\" rx=\"8\" fill=\"var(--metric-bg)\" stroke=\"var(--metric-border)\" stroke-width=\"1\" />");
|
||||||
sb.AppendLine($" <text x=\"12\" y=\"23\" font-size=\"16\">{icon}</text>");
|
sb.AppendLine($" <text x=\"12\" y=\"27\" font-size=\"16\">{icon}</text>");
|
||||||
sb.AppendLine($" <text class=\"stat-val\" x=\"38\" y=\"17\">{value}</text>");
|
sb.AppendLine($" <text class=\"stat-val\" x=\"40\" y=\"20\">{value}</text>");
|
||||||
sb.AppendLine($" <text class=\"stat-lbl\" x=\"38\" y=\"29\">{label}</text>");
|
sb.AppendLine($" <text class=\"stat-lbl\" x=\"40\" y=\"33\">{label}</text>");
|
||||||
sb.AppendLine(" </g>");
|
sb.AppendLine(" </g>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
109
team-stats.svg
Normal file
109
team-stats.svg
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 740 240" width="100%" height="100%">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="teamLangBarClip">
|
||||||
|
<rect x="34" y="125" width="676" height="12" rx="6" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #11111b;
|
||||||
|
--card: #181825;
|
||||||
|
--border: #313244;
|
||||||
|
--text-main: #cdd6f4;
|
||||||
|
--text-sub: #a6adc8;
|
||||||
|
--text-muted: #6c7086;
|
||||||
|
--accent: #89b4fa;
|
||||||
|
--metric-bg: #1e1e2e;
|
||||||
|
--metric-border: #313244;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--bg: #f8fafc;
|
||||||
|
--card: #ffffff;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
--text-main: #0f172a;
|
||||||
|
--text-sub: #334155;
|
||||||
|
--text-muted: #64748b;
|
||||||
|
--accent: #2563eb;
|
||||||
|
--metric-bg: #f1f5f9;
|
||||||
|
--metric-border: #cbd5e1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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); }
|
||||||
|
.lang-lbl { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 10px; font-weight: 500; fill: var(--text-sub); }
|
||||||
|
.project-tag { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; font-size: 10px; fill: var(--text-muted); }
|
||||||
|
</style>
|
||||||
|
<rect class="bg-panel" x="1" y="1" width="738" height="238" />
|
||||||
|
<g transform="translate(24, 20)">
|
||||||
|
<text class="title" x="0" y="14">🏢 WebCore Consulting <tspan font-weight="400" fill="var(--accent)">•</tspan> <tspan font-weight="500" font-size="12" fill="var(--text-sub)">Team & Projekt-Statistiken</tspan></text>
|
||||||
|
<rect x="500" y="0" width="185" height="22" rx="11" fill="var(--metric-bg)" stroke="var(--metric-border)" stroke-width="1" />
|
||||||
|
<circle cx="512" cy="11" r="4" fill="#a855f7" />
|
||||||
|
<text class="subtitle" x="522" y="15" font-weight="600">3 Team-Repositories</text>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(34, 56)">
|
||||||
|
<rect width="216" height="44" rx="8" fill="var(--metric-bg)" stroke="var(--metric-border)" stroke-width="1" />
|
||||||
|
<text x="12" y="27" font-size="16">📁</text>
|
||||||
|
<text class="stat-val" x="40" y="20">3 Repos</text>
|
||||||
|
<text class="stat-lbl" x="40" y="33">Aktive Team-Projekte</text>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(264, 56)">
|
||||||
|
<rect width="216" height="44" rx="8" fill="var(--metric-bg)" stroke="var(--metric-border)" stroke-width="1" />
|
||||||
|
<text x="12" y="27" font-size="16">💾</text>
|
||||||
|
<text class="stat-val" x="40" y="20">1,9 MB</text>
|
||||||
|
<text class="stat-lbl" x="40" y="33">Gesamter Team-Code</text>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(494, 56)">
|
||||||
|
<rect width="216" height="44" rx="8" fill="var(--metric-bg)" stroke="var(--metric-border)" stroke-width="1" />
|
||||||
|
<text x="12" y="27" font-size="16">⚡</text>
|
||||||
|
<text class="stat-val" x="40" y="20">C# (56,2%)</text>
|
||||||
|
<text class="stat-lbl" x="40" y="33">Hauptsprache im Team</text>
|
||||||
|
</g>
|
||||||
|
<!-- Team Languages Progress Bar -->
|
||||||
|
<g clip-path="url(#teamLangBarClip)">
|
||||||
|
<rect x="34.0" y="125" width="379.9" height="12" fill="#a855f7">
|
||||||
|
<title>C#: 56,2% (1,1 MB)</title>
|
||||||
|
</rect>
|
||||||
|
<rect x="413.9" y="125" width="273.1" height="12" fill="#e34c26">
|
||||||
|
<title>HTML: 40,4% (797,4 KB)</title>
|
||||||
|
</rect>
|
||||||
|
<rect x="687.0" y="125" width="15.5" height="12" fill="#563d7c">
|
||||||
|
<title>CSS: 2,3% (45,7 KB)</title>
|
||||||
|
</rect>
|
||||||
|
<rect x="702.6" y="125" width="3.0" height="12" fill="#f1e05a">
|
||||||
|
<title>JavaScript: 0,4% (8,6 KB)</title>
|
||||||
|
</rect>
|
||||||
|
<rect x="705.6" y="125" width="3.0" height="12" fill="#012456">
|
||||||
|
<title>PowerShell: 0,4% (7,6 KB)</title>
|
||||||
|
</rect>
|
||||||
|
<rect x="708.6" y="125" width="3.0" height="12" fill="#89e051">
|
||||||
|
<title>Shell: 0,2% (3,4 KB)</title>
|
||||||
|
</rect>
|
||||||
|
<rect x="711.6" y="125" width="3.0" height="12" fill="#384d54">
|
||||||
|
<title>Dockerfile: 0,1% (2,4 KB)</title>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<!-- Detailed Language Legend -->
|
||||||
|
<g transform="translate(34, 158)">
|
||||||
|
<circle cx="4" cy="4" r="4" fill="#a855f7" />
|
||||||
|
<text class="lang-lbl" x="13" y="7">C#: 56,2% (1,1 MB)</text>
|
||||||
|
<circle cx="229" cy="4" r="4" fill="#e34c26" />
|
||||||
|
<text class="lang-lbl" x="238" y="7">HTML: 40,4% (797,4 KB)</text>
|
||||||
|
<circle cx="454" cy="4" r="4" fill="#563d7c" />
|
||||||
|
<text class="lang-lbl" x="463" y="7">CSS: 2,3% (45,7 KB)</text>
|
||||||
|
<circle cx="4" cy="22" r="4" fill="#f1e05a" />
|
||||||
|
<text class="lang-lbl" x="13" y="25">JavaScript: 0,4% (8,6 KB)</text>
|
||||||
|
<circle cx="229" cy="22" r="4" fill="#012456" />
|
||||||
|
<text class="lang-lbl" x="238" y="25">PowerShell: 0,4% (7,6 KB)</text>
|
||||||
|
<circle cx="454" cy="22" r="4" fill="#89e051" />
|
||||||
|
<text class="lang-lbl" x="463" y="25">Shell: 0,2% (3,4 KB)</text>
|
||||||
|
</g>
|
||||||
|
<!-- Active Team Repositories Footer -->
|
||||||
|
<g transform="translate(34, 215)">
|
||||||
|
<text class="project-tag" x="0" y="0">Team-Repositories: 🚀 ServerManagement (HTML) • 🚀 USBSafe (C#) • 🚀 ServerHub-redesign ()</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.4 KiB |
Loading…
x
Reference in New Issue
Block a user