diff --git a/README.md b/README.md
index 87805b7..754dae6 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,17 @@
+
+
+
+
+
+
+
+
+
+
+
[](https://dotnet.microsoft.com/)
[](https://dotnet.microsoft.com/)
[](https://avaloniaui.net/)
@@ -40,7 +51,7 @@ Ich bin **Nicki (Nicklas)**, angehender Softwareentwickler mit fundiertem Fokus
-## π Live AktivitΓ€ts- & Performance-Dashboards
+## π Live AktivitΓ€ts- & Multi-Tier Dashboards
@@ -96,20 +107,27 @@ Ich bin **Nicki (Nicklas)**, angehender Softwareentwickler mit fundiertem Fokus
----
+
-## π οΈ AusgewΓ€hlte Eigenprojekte
+
+β‘ Tab 3: Live Commit-Radar & AktivitΓ€ts-Feed
-| 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. |
+> *Echtzeit-Feed der letzten Aktionen und Commits ΓΌber alle privaten & Team-Repositories.*
----
+
+- β‘ **feat: add team telemetry dashboard, byte-level language analytics and tabs [skip ci]** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) β’ π *vor 2 Minuten*
+- β‘ **fix(svg): remove utf-8 bom and add xml declaration for gitea typesniffer [skip ci]** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) β’ π *vor 10 Minuten*
+- β‘ **chore(profile): update activity heatmap & stats (22 contributions) [skip ci]** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) β’ π *vor 18 Minuten*
+- β‘ **Initial profile setup** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) β’ π *vor 19 Minuten*
+- β¨ **Repository neu erstellt** auf [`NickiCloud/.profile`](https://git.webcore-consulting.dev/NickiCloud/.profile) β’ π *vor 55 Minuten*
+
-## π‘οΈ Homelab & Server-Infrastruktur
+
+
+
+
+
+π‘οΈ Tab 4: Homelab, Server-Infrastruktur & Telemetrie
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -130,11 +148,26 @@ Ich bin **Nicki (Nicklas)**, angehender Softwareentwickler mit fundiertem Fokus
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
-- **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`
+| Service | Host / Umgebung | Funktion | Status |
+| :--- | :--- | :--- | :---: |
+| π¦ **Gitea Service** | `git.webcore-consulting.dev` | Versionsverwaltung, API & Git LFS | `π’ ONLINE` |
+| β‘ **Gitea Act-Runner** | Docker `catthehacker/ubuntu:act` | Autonome CI/CD-Pipeline (.NET 10) | `π’ AKTIV` |
+| βοΈ **Mailcow Suite** | Docker Stack / Debian 12 | Enterprise Mail (DKIM, SPF, DMARC) | `π’ ONLINE` |
+| π‘οΈ **Nginx Proxy** | Reverse Proxy Host | SSL/TLS 1.3 Offloading & HSTS | `π’ AKTIV` |
+| π **Security Wall** | UFW Firewall & fail2ban | SSH & HTTP Brute-Force Abwehr | `π’ SCHARF` |
+
+
+
+---
+
+## π οΈ 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. |
---
diff --git a/heatmap.svg b/heatmap.svg
index d30fe62..66bbbf0 100644
--- a/heatmap.svg
+++ b/heatmap.svg
@@ -74,7 +74,7 @@
π¦ NickiCloud β’ PersΓΆnliche AktivitΓ€t
- 24 Commits im letzten Jahr
+ 25 Commits im letzten Jahr
Sep
@@ -1173,7 +1173,7 @@
01.09.2026: 9 Commits
- 02.09.2026: 8 Commits
+ 02.09.2026: 9 Commits
03.09.2026: 0 Commits
@@ -1192,7 +1192,7 @@
π¦
- 24
+ 25
Jahres-BeitrΓ€ge
@@ -1209,11 +1209,11 @@
-
- C#: 57,0%
+
+ C#: 57,3%
-
- HTML: 39,7%
+
+ HTML: 39,4%
CSS: 2,3%
@@ -1228,9 +1228,9 @@
- C# 57,0%
+ C# 57,3%
- HTML 39,7%
+ HTML 39,4%
CSS 2,3%
diff --git a/src/ProfileManager/Models/ActivityFeed.cs b/src/ProfileManager/Models/ActivityFeed.cs
new file mode 100644
index 0000000..4905743
--- /dev/null
+++ b/src/ProfileManager/Models/ActivityFeed.cs
@@ -0,0 +1,82 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace ProfileManager.Models;
+
+public sealed class ActivityCommit
+{
+ [JsonPropertyName("Sha1")]
+ public string Sha1 { get; set; } = string.Empty;
+
+ [JsonPropertyName("Message")]
+ public string Message { get; set; } = string.Empty;
+
+ [JsonPropertyName("AuthorName")]
+ public string AuthorName { get; set; } = string.Empty;
+
+ [JsonPropertyName("Timestamp")]
+ public string Timestamp { get; set; } = string.Empty;
+}
+
+public sealed class ActivityContent
+{
+ [JsonPropertyName("Commits")]
+ public List? Commits { get; set; }
+
+ [JsonPropertyName("HeadCommit")]
+ public ActivityCommit? HeadCommit { get; set; }
+}
+
+public sealed class ActivityItem
+{
+ [JsonPropertyName("id")]
+ public long Id { get; set; }
+
+ [JsonPropertyName("op_type")]
+ public string OpType { get; set; } = string.Empty;
+
+ [JsonPropertyName("repo")]
+ public GiteaRepo? Repo { get; set; }
+
+ [JsonPropertyName("ref_name")]
+ public string? RefName { get; set; }
+
+ [JsonPropertyName("created")]
+ public DateTime Created { get; set; }
+
+ [JsonPropertyName("content")]
+ public string? RawContent { get; set; }
+
+ [JsonIgnore]
+ public string DisplayMessage
+ {
+ get
+ {
+ if (!string.IsNullOrWhiteSpace(RawContent))
+ {
+ try
+ {
+ var content = JsonSerializer.Deserialize(RawContent);
+ var commit = content?.HeadCommit ?? content?.Commits?.FirstOrDefault();
+ if (commit != null && !string.IsNullOrWhiteSpace(commit.Message))
+ {
+ var firstLine = commit.Message.Trim().Split('\n')[0].Trim();
+ return firstLine;
+ }
+ }
+ catch
+ {
+ // Ignore parse error
+ }
+ }
+
+ return OpType switch
+ {
+ "commit_repo" => "Neuer Commit gepusht",
+ "create_repo" => "Repository neu erstellt",
+ "create_branch" => "Branch angelegt",
+ _ => "AktivitΓ€t auf Repository"
+ };
+ }
+ }
+}
diff --git a/src/ProfileManager/Models/ProfileStats.cs b/src/ProfileManager/Models/ProfileStats.cs
index a70009d..7341413 100644
--- a/src/ProfileManager/Models/ProfileStats.cs
+++ b/src/ProfileManager/Models/ProfileStats.cs
@@ -53,4 +53,5 @@ public sealed class ProfileStats
public required List OverallLanguages { get; init; }
public required List PersonalRepos { get; init; }
public required TeamStats? TeamStats { get; init; }
+ public required List RecentActivities { get; init; }
}
diff --git a/src/ProfileManager/Program.cs b/src/ProfileManager/Program.cs
index 925f8eb..d53759f 100644
--- a/src/ProfileManager/Program.cs
+++ b/src/ProfileManager/Program.cs
@@ -38,16 +38,18 @@ internal static class Program
{
var client = new GiteaApiClient(apiUrl, string.IsNullOrWhiteSpace(token) ? null : token);
- // Step 1: Parallel retrieval of Heatmap, Accessible Repos, and Organizations
+ // Step 1: Parallel retrieval of Heatmap, Accessible Repos, Organizations, and Activity Feed
var heatmapTask = client.GetHeatmapAsync(username);
var reposTask = client.GetAllAccessibleReposAsync(username);
var orgsTask = client.GetUserOrganizationsAsync(username);
+ var activitiesTask = client.GetRecentActivitiesAsync(username);
- await Task.WhenAll(heatmapTask, reposTask, orgsTask);
+ await Task.WhenAll(heatmapTask, reposTask, orgsTask, activitiesTask);
var heatmap = await heatmapTask;
var repos = await reposTask;
var orgs = await orgsTask;
+ var activities = await activitiesTask;
// Step 2: Fetch detailed language byte counts per repository
Console.WriteLine("[API] Fetching byte-level language breakdown for all repositories...");
@@ -59,8 +61,8 @@ internal static class Program
await Task.WhenAll(langTasks);
// Step 3: Calculate multi-tier statistics
- Console.WriteLine("[Stats] Calculating Personal, Team & Byte metrics...");
- var stats = StatsCalculator.Calculate(heatmap, repos, orgs, username);
+ Console.WriteLine("[Stats] Calculating Personal, Team, Byte & Activity metrics...");
+ var stats = StatsCalculator.Calculate(heatmap, repos, orgs, activities, username);
Console.WriteLine($"[Stats] 365d Contributions: {stats.TotalContributions365}");
Console.WriteLine($"[Stats] Current Streak : {stats.CurrentStreak} days");
diff --git a/src/ProfileManager/Services/GiteaApiClient.cs b/src/ProfileManager/Services/GiteaApiClient.cs
index 58a5b46..4941bdc 100644
--- a/src/ProfileManager/Services/GiteaApiClient.cs
+++ b/src/ProfileManager/Services/GiteaApiClient.cs
@@ -176,6 +176,35 @@ public sealed class GiteaApiClient
return [];
}
+ public async Task> GetRecentActivitiesAsync(string username, CancellationToken cancellationToken = default)
+ {
+ var url = $"{_baseUrl}/users/{Uri.EscapeDataString(username)}/activities/feeds?limit=15";
+ Console.WriteLine($"[API] Fetching recent activities from: {url}");
+
+ try
+ {
+ using var response = await _httpClient.GetAsync(url, cancellationToken);
+ if (response.IsSuccessStatusCode)
+ {
+ var json = await response.Content.ReadAsStringAsync(cancellationToken);
+ var items = JsonSerializer.Deserialize>(json, JsonOptions);
+ if (items != null && items.Count > 0)
+ {
+ Console.WriteLine($"[API] Retrieved {items.Count} recent activity feed items.");
+ return items;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"[API-WARN] Activity feed error: {ex.Message}.");
+ Console.ResetColor();
+ }
+
+ return [];
+ }
+
private static List GenerateMockHeatmap()
{
var items = new List();
diff --git a/src/ProfileManager/Services/ReadmeInjector.cs b/src/ProfileManager/Services/ReadmeInjector.cs
index 9ff9c66..8c2bb1a 100644
--- a/src/ProfileManager/Services/ReadmeInjector.cs
+++ b/src/ProfileManager/Services/ReadmeInjector.cs
@@ -14,6 +14,12 @@ public static class ReadmeInjector
private const string StartLangMarker = "";
private const string EndLangMarker = "";
+ private const string StartActivityMarker = "";
+ private const string EndActivityMarker = "";
+
+ private const string StartTelemetryMarker = "";
+ private const string EndTelemetryMarker = "";
+
public static bool InjectAll(string readmePath, ProfileStats stats)
{
if (!File.Exists(readmePath))
@@ -95,9 +101,47 @@ public static class ReadmeInjector
}
});
+ // 4. Inject Live Activity Stream
+ content = ReplaceSection(content, StartActivityMarker, EndActivityMarker, sb =>
+ {
+ if (stats.RecentActivities.Count == 0)
+ {
+ sb.AppendLine("*Aktuell keine kΓΌrzlichen AktivitΓ€ten im Feed.*");
+ }
+ else
+ {
+ foreach (var act in stats.RecentActivities.Take(5))
+ {
+ var repoName = act.Repo?.FullName ?? "Repository";
+ var repoUrl = act.Repo?.HtmlUrl ?? $"https://git.webcore-consulting.dev/{repoName}";
+ var relTime = FormatRelativeTime(act.Created);
+ var icon = act.OpType switch
+ {
+ "commit_repo" => "β‘",
+ "create_repo" => "β¨",
+ _ => "π"
+ };
+
+ sb.AppendLine($"- {icon} **{act.DisplayMessage}** auf [`{repoName}`]({repoUrl}) β’ π *{relTime}*");
+ }
+ }
+ });
+
+ // 5. Inject Telemetry Header
+ content = ReplaceSection(content, StartTelemetryMarker, EndTelemetryMarker, sb =>
+ {
+ var now = DateTime.UtcNow.ToString("dd.MM.yyyy HH:mm");
+ sb.AppendLine($"");
+ sb.AppendLine($"
");
+ sb.AppendLine($"
");
+ sb.AppendLine($"
");
+ sb.AppendLine($"
");
+ sb.AppendLine($"
");
+ });
+
var utf8WithoutBom = new UTF8Encoding(false);
File.WriteAllText(readmePath, content, utf8WithoutBom);
- Console.WriteLine($"[README] Successfully updated all dynamic sections in {readmePath}");
+ Console.WriteLine($"[README] Successfully updated all 5 dynamic sections in {readmePath}");
return true;
}
@@ -118,4 +162,15 @@ public static class ReadmeInjector
return source.Substring(0, startIndex) + sb + source.Substring(endIndex + endMarker.Length);
}
+
+ private static string FormatRelativeTime(DateTime dateTime)
+ {
+ var diff = DateTime.UtcNow - dateTime.ToUniversalTime();
+ if (diff.TotalMinutes < 1) return "gerade eben";
+ if (diff.TotalMinutes < 60) return $"vor {(int)diff.TotalMinutes} Minuten";
+ if (diff.TotalHours < 24) return $"vor {(int)diff.TotalHours} Stunden";
+ if (diff.TotalDays < 2) return "gestern";
+ if (diff.TotalDays < 30) return $"vor {(int)diff.TotalDays} Tagen";
+ return dateTime.ToString("dd.MM.yyyy");
+ }
}
diff --git a/src/ProfileManager/Services/StatsCalculator.cs b/src/ProfileManager/Services/StatsCalculator.cs
index 35338de..0b6ab9f 100644
--- a/src/ProfileManager/Services/StatsCalculator.cs
+++ b/src/ProfileManager/Services/StatsCalculator.cs
@@ -26,6 +26,7 @@ public static class StatsCalculator
List heatmapItems,
List allRepos,
List orgs,
+ List recentActivities,
string personalUsername = "NickiCloud",
DateOnly? referenceDate = null)
{
@@ -162,7 +163,8 @@ public static class StatsCalculator
Days = days,
OverallLanguages = overallLanguages,
PersonalRepos = personalRepos,
- TeamStats = teamStats
+ TeamStats = teamStats,
+ RecentActivities = recentActivities
};
}