Migrate all repos into monorepo context folders

Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
      Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
      Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)

Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
This commit is contained in:
2026-06-30 20:39:52 +02:00
parent 2f2b295531
commit a5f8fb49ab
1717 changed files with 447332 additions and 0 deletions
@@ -0,0 +1,57 @@
"""Prueft ob das Remote-Repo Commits hat."""
import urllib.request, ssl, json
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
TOKEN = secrets.get("GITLAB_TOKEN_AUDIT", "")
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
# Pruefe Projekt
url = "https://git.tech.rz.db.de/api/v4/projects/AndreKnie%2Fproject-audit"
req = urllib.request.Request(url)
req.add_header("PRIVATE-TOKEN", TOKEN)
try:
resp = urllib.request.urlopen(req, context=ctx, timeout=15)
proj = json.loads(resp.read().decode("utf-8"))
print(f"Projekt: {proj['path_with_namespace']}")
print(f"Default Branch: {proj.get('default_branch', '?')}")
print(f"Letzter Push: {proj.get('last_activity_at', '?')}")
print(f"Leer: {proj.get('empty_repo', '?')}")
except Exception as e:
print(f"Projekt-Fehler: {e}")
exit(1)
# Pruefe Commits
print()
url2 = "https://git.tech.rz.db.de/api/v4/projects/AndreKnie%2Fproject-audit/repository/commits?per_page=5"
req2 = urllib.request.Request(url2)
req2.add_header("PRIVATE-TOKEN", TOKEN)
try:
resp2 = urllib.request.urlopen(req2, context=ctx, timeout=15)
commits = json.loads(resp2.read().decode("utf-8"))
print(f"Remote Commits: {len(commits)}")
for c in commits:
print(f" {c['short_id']} | {c['created_at'][:16]} | {c['title'][:50]}")
except Exception as e:
print(f"Commits-Fehler: {e}")
# Pruefe Branches
print()
url3 = "https://git.tech.rz.db.de/api/v4/projects/AndreKnie%2Fproject-audit/repository/branches"
req3 = urllib.request.Request(url3)
req3.add_header("PRIVATE-TOKEN", TOKEN)
try:
resp3 = urllib.request.urlopen(req3, context=ctx, timeout=15)
branches = json.loads(resp3.read().decode("utf-8"))
print(f"Branches: {len(branches)}")
for b in branches:
print(f" {b['name']}: {b['commit']['short_id']} ({b['commit']['created_at'][:16]})")
except Exception as e:
print(f"Branches-Fehler: {e}")
@@ -0,0 +1,77 @@
<#
.SYNOPSIS
Prueft welche Unterseiten unter der Analyse-Stammseite existieren
#>
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
$SpaceKey = "BES"
$ParentPageId = "581013135"
$ApiBase = "$ConfluenceUrl/rest/api"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Token aus .secrets laden
$SecretsPath = Join-Path $ScriptDir "..\.secrets"
$PAT = $null
if (Test-Path $SecretsPath) {
Get-Content $SecretsPath | ForEach-Object {
if ($_ -match "^CONFLUENCE_TOKEN=(.+)$") {
$val = $Matches[1].Trim()
if ($val -ne "HIER_TOKEN_EINTRAGEN") { $PAT = $val }
}
}
}
if (-not $PAT) { $PAT = Read-Host "Confluence PAT" }
$Headers = @{ "Authorization" = "Bearer $PAT"; "Content-Type" = "application/json" }
Write-Host "=== Confluence Seiten-Check ===" -ForegroundColor Cyan
Write-Host "Parent: $ParentPageId"
Write-Host ""
# Alle Unterseiten laden
$Uri = "$ApiBase/content/$ParentPageId/child/page?limit=50&expand=version"
try {
$Resp = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
$Data = $Resp.Content | ConvertFrom-Json
Write-Host "Gefundene Unterseiten: $($Data.results.Count)" -ForegroundColor Green
Write-Host ""
Write-Host ("{0,-5} {1,-55} {2,-10} {3}" -f "Ver", "Titel", "ID", "Status")
Write-Host ("-" * 90)
$Data.results | Sort-Object { $_.title } | ForEach-Object {
$ver = $_.version.number
$title = $_.title
$id = $_.id
Write-Host ("{0,-5} {1,-55} {2,-10}" -f "v$ver", $title, $id)
}
} catch {
Write-Host "FEHLER: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host ""
Write-Host "=== Titel-Suche fuer Seiten 1-5 ===" -ForegroundColor Cyan
$Titles = @(
"1. Uebersicht und Gesamtbild",
"2. Identifizierte Problemfelder",
"3. Technische Roadmap 2026 - Zusammenfassung",
"4. GitLab Projektlandschaft",
"5. Abkuerzungsverzeichnis"
)
foreach ($t in $Titles) {
$SearchUri = "$ApiBase/content?spaceKey=$SpaceKey&title=$([Uri]::EscapeDataString($t))&type=page"
try {
$SearchResp = Invoke-WebRequest -Uri $SearchUri -Headers $Headers -UseBasicParsing -ErrorAction Stop
$SearchData = $SearchResp.Content | ConvertFrom-Json
if ($SearchData.results -and $SearchData.results.Count -gt 0) {
Write-Host " GEFUNDEN: $t (ID: $($SearchData.results[0].id))" -ForegroundColor Green
} else {
Write-Host " NICHT GEFUNDEN: $t" -ForegroundColor Red
}
} catch {
Write-Host " FEHLER bei: $t - $($_.Exception.Message)" -ForegroundColor Red
}
}
@@ -0,0 +1,46 @@
<#
.SYNOPSIS
Loescht die Test-Seite "TEST Umlaut-Pruefung" (ID: 583338908)
#>
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
$ApiBase = "$ConfluenceUrl/rest/api"
$PageId = "583338908"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Token aus .secrets laden
$SecretsPath = Join-Path $ScriptDir "..\.secrets"
$PAT = $null
if (Test-Path $SecretsPath) {
Get-Content $SecretsPath | ForEach-Object {
if ($_ -match "^CONFLUENCE_TOKEN=(.+)$") {
$val = $Matches[1].Trim()
if ($val -ne "HIER_TOKEN_EINTRAGEN") { $PAT = $val }
}
}
}
if (-not $PAT) { $PAT = Read-Host "Confluence PAT" }
$Headers = @{ "Authorization" = "Bearer $PAT"; "Content-Type" = "application/json" }
Write-Host "=== Loesche Test-Seite ===" -ForegroundColor Yellow
Write-Host " Seite: TEST Umlaut-Pruefung (ID: $PageId)"
# Pruefen ob Seite existiert
try {
$Resp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId" -Headers $Headers -UseBasicParsing -ErrorAction Stop
$Data = $Resp.Content | ConvertFrom-Json
Write-Host " Gefunden: $($Data.title)" -ForegroundColor Green
} catch {
Write-Host " Seite nicht gefunden oder bereits geloescht." -ForegroundColor DarkGray
exit 0
}
# Loeschen
try {
Invoke-WebRequest -Uri "$ApiBase/content/$PageId" -Method Delete -Headers $Headers -UseBasicParsing -ErrorAction Stop
Write-Host " GELOESCHT!" -ForegroundColor Green
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
}
@@ -0,0 +1,257 @@
<#
.SYNOPSIS
Confluence Space Export fuer pathOS / Bestellsystem
.DESCRIPTION
Exportiert die Seitenstruktur und Inhalte aus dem Confluence Space BES.
Kompatibel mit PowerShell 5.1 - keine Sonderzeichen.
#>
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
$SpaceKey = "BES"
$RootPageId = "196749641"
$ApiBase = "$ConfluenceUrl/rest/api"
$PerPage = 50
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$OutputDir = Join-Path (Split-Path -Parent $ScriptDir) "data\confluence-export"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "============================================================"
Write-Host " pathOS / Bestellsystem - Confluence Export"
Write-Host "============================================================"
Write-Host " Confluence: $ConfluenceUrl"
Write-Host " Space: $SpaceKey"
Write-Host ""
Write-Host "Authentifizierung:"
Write-Host " Option 1: Personal Access Token (PAT) - empfohlen"
Write-Host " Option 2: Benutzername + Passwort"
Write-Host ""
$AuthMethod = Read-Host "PAT oder Passwort? (pat/pw)"
if ($AuthMethod -eq "pw") {
$Username = Read-Host "Benutzername"
$Password = Read-Host "Passwort"
$Pair = "${Username}:${Password}"
$Bytes = [System.Text.Encoding]::ASCII.GetBytes($Pair)
$Base64 = [System.Convert]::ToBase64String($Bytes)
$Headers = @{ "Authorization" = "Basic $Base64" }
} else {
$PAT = Read-Host "Personal Access Token"
$Headers = @{ "Authorization" = "Bearer $PAT" }
}
function Safe-String($val) {
if ($null -eq $val) { return "" }
return [string]$val
}
function Invoke-ConfluenceApi {
param([string]$Endpoint, [hashtable]$Params = @{})
$QS = ($Params.GetEnumerator() | ForEach-Object {
"$($_.Key)=$([Uri]::EscapeDataString($_.Value))"
}) -join "&"
$Uri = "$ApiBase$Endpoint"
if ($QS) { $Uri = "$Uri`?$QS" }
try {
$Resp = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
return ($Resp.Content | ConvertFrom-Json)
} catch {
Write-Host " FEHLER bei $Uri : $($_.Exception.Message)" -ForegroundColor Red
return $null
}
}
function Get-ChildPages {
param([string]$PageId)
$AllChildren = @()
$Start = 0
do {
$Result = Invoke-ConfluenceApi -Endpoint "/content/$PageId/child/page" -Params @{
start = "$Start"
limit = "$PerPage"
expand = "version"
}
if ($null -eq $Result) { break }
$Pages = $Result.results
if ($null -eq $Pages -or $Pages.Count -eq 0) { break }
$AllChildren += $Pages
$Start += $PerPage
$Size = 0
if ($Result.size) { $Size = $Result.size }
} while ($Size -ge $PerPage)
return $AllChildren
}
function Get-PageContent {
param([string]$PageId)
return Invoke-ConfluenceApi -Endpoint "/content/$PageId" -Params @{
expand = "body.storage,version,ancestors,metadata.labels,space"
}
}
function Convert-HtmlToText($html) {
if ($null -eq $html) { return "" }
$text = $html -replace '<br\s*/?>', "`n"
$text = $text -replace '<p[^>]*>', "`n"
$text = $text -replace '</p>', ""
$text = $text -replace '<li[^>]*>', "`n- "
$text = $text -replace '<h[1-6][^>]*>', "`n## "
$text = $text -replace '</h[1-6]>', "`n"
$text = $text -replace '<tr[^>]*>', "`n| "
$text = $text -replace '<td[^>]*>', " | "
$text = $text -replace '<th[^>]*>', " | "
$text = $text -replace '<[^>]+>', ''
Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue
try { $text = [System.Web.HttpUtility]::HtmlDecode($text) } catch {}
return $text.Trim()
}
function Export-PageTree {
param(
[string]$PageId,
[string]$ParentPath,
[int]$Depth = 0,
[ref]$Counter
)
$Indent = " " * $Depth
$Counter.Value++
$Num = $Counter.Value
$Page = Get-PageContent -PageId $PageId
if ($null -eq $Page) { return $null }
$Title = Safe-String $Page.title
Write-Host "$Indent[$Num] $Title"
$Labels = @()
if ($Page.metadata -and $Page.metadata.labels -and $Page.metadata.labels.results) {
$Labels = @($Page.metadata.labels.results | ForEach-Object { $_.name })
}
$VersionNum = 0
if ($Page.version) { $VersionNum = $Page.version.number }
$PageInfo = [PSCustomObject]@{
id = $Page.id
title = $Title
version = $VersionNum
labels = $Labels
depth = $Depth
path = "$ParentPath/$Title"
children = @()
}
# Inhalt speichern
$HtmlContent = ""
$TextContent = ""
if ($Page.body -and $Page.body.storage) {
$HtmlContent = Safe-String $Page.body.storage.value
$TextContent = Convert-HtmlToText $HtmlContent
}
$SafeTitle = $Title -replace '[\\/:*?"<>|]', '_'
if ($SafeTitle.Length -gt 80) { $SafeTitle = $SafeTitle.Substring(0, 80) }
$PageDir = Join-Path $OutputDir "pages"
# Markdown
$MdLines = @()
$MdLines += "# $Title"
$MdLines += ""
$MdLines += "> Confluence Page ID: $($Page.id)"
$MdLines += "> Version: $VersionNum"
$MdLines += "> Pfad: $($PageInfo.path)"
$MdLines += "> Labels: $(($Labels) -join ', ')"
$MdLines += ""
$MdLines += "---"
$MdLines += ""
$MdLines += $TextContent
$MdPath = Join-Path $PageDir "$($Page.id)_$SafeTitle.md"
$MdLines -join "`n" | Out-File -FilePath $MdPath -Encoding UTF8
# HTML
$HtmlPath = Join-Path $PageDir "$($Page.id)_$SafeTitle.html"
$HtmlContent | Out-File -FilePath $HtmlPath -Encoding UTF8
# Kindseiten
$Children = @(Get-ChildPages -PageId $PageId)
$ChildInfos = @()
foreach ($Child in $Children) {
$ChildInfo = Export-PageTree -PageId $Child.id -ParentPath $PageInfo.path -Depth ($Depth + 1) -Counter $Counter
if ($null -ne $ChildInfo) {
$ChildInfos += $ChildInfo
}
}
$PageInfo.children = $ChildInfos
return $PageInfo
}
function Write-TreeLines {
param($Node, [int]$IndentLevel = 0, [System.Collections.ArrayList]$Lines)
$Spaces = " " * $IndentLevel
$null = $Lines.Add("${Spaces}+-- $($Node.title) (ID: $($Node.id), v$($Node.version))")
foreach ($Child in $Node.children) {
Write-TreeLines -Node $Child -IndentLevel ($IndentLevel + 1) -Lines $Lines
}
}
# === MAIN ===
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
New-Item -ItemType Directory -Path "$OutputDir\pages" -Force | Out-Null
Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue
try {
Write-Host ""
Write-Host ">> Teste Verbindung..." -ForegroundColor Yellow
$TestResult = Invoke-ConfluenceApi -Endpoint "/space/$SpaceKey"
if ($null -eq $TestResult) {
Write-Host " Verbindung fehlgeschlagen. Token/Credentials pruefen." -ForegroundColor Red
exit 1
}
Write-Host " Space gefunden: $($TestResult.name) ($SpaceKey)" -ForegroundColor Green
Write-Host ""
Write-Host ">> Exportiere Seitenbaum ab Seite $RootPageId..." -ForegroundColor Yellow
$Counter = [ref]0
$Tree = Export-PageTree -PageId $RootPageId -ParentPath "" -Depth 0 -Counter $Counter
Write-Host ""
Write-Host ">> $($Counter.Value) Seiten exportiert" -ForegroundColor Green
# Baumstruktur
$TreeLines = [System.Collections.ArrayList]::new()
Write-TreeLines -Node $Tree -Lines $TreeLines
$TreePath = Join-Path $OutputDir "page-tree.md"
$TreeOut = @()
$TreeOut += "# Confluence Seitenbaum - pathOS ($SpaceKey)"
$TreeOut += ""
$TreeOut += "> Exportiert: $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
$TreeOut += "> Seiten: $($Counter.Value)"
$TreeOut += ""
$TreeOut += $TreeLines
$TreeOut -join "`n" | Out-File -FilePath $TreePath -Encoding UTF8
Write-Host " Baumstruktur: $TreePath"
# JSON-Index
$JsonPath = Join-Path $OutputDir "page-index.json"
$Tree | ConvertTo-Json -Depth 20 | Out-File -FilePath $JsonPath -Encoding UTF8
Write-Host " JSON-Index: $JsonPath"
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " FERTIG! $($Counter.Value) Seiten exportiert." -ForegroundColor Green
Write-Host " Ergebnisse: $OutputDir" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
} catch {
Write-Host ""
Write-Host "FEHLER: $($_.Exception.Message)" -ForegroundColor Red
if ($_.Exception.Message -match "401|403") {
Write-Host " Authentifizierung fehlgeschlagen." -ForegroundColor Yellow
}
if ($_.Exception.Message -match "resolve|Remotename") {
Write-Host " Verbindung fehlgeschlagen. VPN aktiv?" -ForegroundColor Yellow
}
exit 1
}
@@ -0,0 +1,440 @@
"""Pusht Meilensteinplan 12 Monate als Confluence-Seite 27."""
import urllib.request, json, ssl, urllib.parse
from pathlib import Path
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
TOKEN = secrets.get("CONFLUENCE_TOKEN", "")
BASE = "https://arija-confluence.jaas.service.deutschebahn.com/rest/api"
SPACE = "BES"
PARENT = "581013135"
TITLE = "27. Meilensteinplan 12 Monate (Jun 2026 - Mai 2027)"
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
html = """<h1>Meilensteinplan pathOS &mdash; 12 Monate</h1>
<p><strong>Stand:</strong> 2026-05-12 | <strong>Status:</strong> ENTWURF<br/>
<strong>Vision:</strong> pathOS als Kernplattform eines integrierten InfraGO-Bestellportals<br/>
<strong>Priorit&auml;ten:</strong> Stabilit&auml;t &rarr; Regulatorik &rarr; Kunden-Features<br/>
<strong>Kapazit&auml;t:</strong> ~400 Issues/Monat (alle Teams), ~35 Personen</p>
<ac:structured-macro ac:name="info"><ac:rich-text-body>
<p><strong>4 Phasen, 13 Meilensteine:</strong> Stabilisieren &rarr; Normalbetrieb + Regulatorik &rarr; Skalieren + Reorg &rarr; Integrieren + Vision</p>
</ac:rich-text-body></ac:structured-macro>
<h2>Grafische &Uuml;bersicht: Timeline</h2>
<table style="width:100%; border-collapse:collapse; font-size:12px;">
<tr style="background:#f0f0f0;">
<th style="width:18%; padding:4px; border:1px solid #ccc;">Meilenstein</th>
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Jun</th>
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Jul</th>
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Aug</th>
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Sep</th>
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Okt</th>
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Nov</th>
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Dez</th>
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Jan</th>
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Feb</th>
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">M&auml;r</th>
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Apr</th>
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Mai</th>
</tr>
"""
html += """<tr style="background:#fff3e0; font-weight:bold;">
<td style="padding:2px 4px; border:1px solid #ccc;" colspan="13">Phase 1: STABILISIEREN</td>
</tr>
<tr>
<td style="padding:4px; border:1px solid #ccc;">M1: Spring Boot 4 &#9888;</td>
<td style="background:#e53935; border:1px solid #ccc; text-align:center; color:white;">&#9733;</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
</tr>
<tr>
<td style="padding:4px; border:1px solid #ccc;">M2: Bug-Rate &lt;25%</td>
<td style="background:#ff9800; border:1px solid #ccc;"></td>
<td style="background:#ff9800; border:1px solid #ccc; text-align:center; color:white;">&#9733;</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
</tr>
<tr>
<td style="padding:4px; border:1px solid #ccc;">M3: F&uuml;hrungswechsel</td>
<td style="border:1px solid #ccc;"></td>
<td style="background:#1565c0; border:1px solid #ccc; text-align:center; color:white;">&#9733;</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
</tr>
"""
html += """<tr style="background:#e3f2fd; font-weight:bold;">
<td style="padding:2px 4px; border:1px solid #ccc;" colspan="13">Phase 2: NORMALBETRIEB + REGULATORIK</td>
</tr>
<tr>
<td style="padding:4px; border:1px solid #ccc;">M4: SL Silber Plus &#9888;</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
<td style="background:#e53935; border:1px solid #ccc; text-align:center; color:white;">&#9733;</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
</tr>
<tr>
<td style="padding:4px; border:1px solid #ccc;">M5: Abrechnung TTT</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
<td style="background:#ff9800; border:1px solid #ccc;"></td>
<td style="background:#ff9800; border:1px solid #ccc;"></td>
<td style="background:#ff9800; border:1px solid #ccc; text-align:center; color:white;">&#9733;</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
</tr>
<tr>
<td style="padding:4px; border:1px solid #ccc;">M6: NA&Auml; produktiv</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
<td style="background:#4caf50; border:1px solid #ccc;"></td>
<td style="background:#4caf50; border:1px solid #ccc; text-align:center; color:white;">&#9733;</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
</tr>
<tr>
<td style="padding:4px; border:1px solid #ccc;">M7: OpsDev formiert</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
<td style="background:#7b1fa2; border:1px solid #ccc;"></td>
<td style="background:#7b1fa2; border:1px solid #ccc; text-align:center; color:white;">&#9733;</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
</tr>
"""
html += """<tr style="background:#e8f5e9; font-weight:bold;">
<td style="padding:2px 4px; border:1px solid #ccc;" colspan="13">Phase 3: SKALIEREN + REORG</td>
</tr>
<tr>
<td style="padding:4px; border:1px solid #ccc;">M8: FplJ 28 &#9888;</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
<td style="background:#e53935; border:1px solid #ccc;"></td>
<td style="background:#e53935; border:1px solid #ccc;"></td>
<td style="background:#e53935; border:1px solid #ccc; text-align:center; color:white;">&#9733;</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
</tr>
<tr>
<td style="padding:4px; border:1px solid #ccc;">M9: Rechnungsbeginn TTT &#9888;</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
<td style="background:#e53935; border:1px solid #ccc; text-align:center; color:white;">&#9733;</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
</tr>
<tr>
<td style="padding:4px; border:1px solid #ccc;">M10: Reorg abgeschlossen</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
<td style="background:#7b1fa2; border:1px solid #ccc;"></td>
<td style="background:#7b1fa2; border:1px solid #ccc;"></td>
<td style="background:#7b1fa2; border:1px solid #ccc; text-align:center; color:white;">&#9733;</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
</tr>
"""
html += """<tr style="background:#fce4ec; font-weight:bold;">
<td style="padding:2px 4px; border:1px solid #ccc;" colspan="13">Phase 4: INTEGRIEREN + VISION</td>
</tr>
<tr>
<td style="padding:4px; border:1px solid #ccc;">M11: TrassenOrder-Integr.</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
<td style="background:#1565c0; border:1px solid #ccc; text-align:center; color:white;">&#9733;</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
</tr>
<tr>
<td style="padding:4px; border:1px solid #ccc;">M12: Plattform-Vision</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
<td style="background:#1565c0; border:1px solid #ccc;"></td>
<td style="background:#1565c0; border:1px solid #ccc; text-align:center; color:white;">&#9733;</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
</tr>
<tr>
<td style="padding:4px; border:1px solid #ccc;">M13: Kundenzufriedenheit</td>
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
<td style="background:#4caf50; border:1px solid #ccc;"></td>
<td style="background:#4caf50; border:1px solid #ccc;"></td>
<td style="background:#4caf50; border:1px solid #ccc; text-align:center; color:white;">&#9733;</td>
</tr>
</table>
<p><strong>Legende:</strong> &#9733; = Deadline | &#9888; = Harte externe Deadline |
<span style="background:#e53935; color:white; padding:2px 6px;">MUSS</span>
<span style="background:#ff9800; color:white; padding:2px 6px;">HOCH</span>
<span style="background:#4caf50; color:white; padding:2px 6px;">Feature</span>
<span style="background:#1565c0; color:white; padding:2px 6px;">Strategie</span>
<span style="background:#7b1fa2; color:white; padding:2px 6px;">Reorg</span></p>
"""
html += """
<h2>Phase 1: STABILISIEREN (Jun &ndash; Jul 2026)</h2>
<ac:structured-macro ac:name="panel"><ac:parameter ac:name="title">Ziel</ac:parameter><ac:rich-text-body>
<p>Technische Schulden abbauen, Hypercare beenden, F&uuml;hrungswechsel vorbereiten</p>
</ac:rich-text-body></ac:structured-macro>
<h3>M1: Spring Boot 4 abgeschlossen (30. Jun 2026) &#9888;</h3>
<table>
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
<tr><td>SV, AV, IFP auf Spring Boot 4</td><td>CIB (Saurav, Hans-Henning)</td><td>&#128260; In Arbeit</td></tr>
<tr><td>CI, AV-Trasse auf Spring Boot 4</td><td>Zero (O2CZERO-7043)</td><td>&#128260; In Arbeit</td></tr>
<tr><td>Alle Services auf Java 21</td><td>Alle</td><td>Teilweise</td></tr>
<tr><td>Regressionstests nach Upgrade</td><td>QA + Teams</td><td>Offen</td></tr>
</table>
<ac:structured-macro ac:name="warning"><ac:rich-text-body>
<p><strong>Risiko:</strong> EOL 3.5 ist Juni 2026 &mdash; kein Security-Support mehr danach. MUSS-Termin.</p>
</ac:rich-text-body></ac:structured-macro>
<h3>M2: Bug-Rate Portal unter 25% (31. Jul 2026)</h3>
<table>
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
<tr><td>Entkopplung Zuglaufpunkte/Zugcharakteristik (Hauptursache)</td><td>404</td><td>Offen</td></tr>
<tr><td>Fehlermeldungen-Konzept umsetzen</td><td>404</td><td>Offen</td></tr>
<tr><td>E2E-Tests (Playwright) f&uuml;r Top-10-Flows</td><td>404</td><td>Offen</td></tr>
<tr><td>Testumgebung mit TPN dauerhaft verf&uuml;gbar</td><td>404 + OPs</td><td>Offen</td></tr>
</table>
<h3>M3: F&uuml;hrungswechsel + Hypercare-Ende (Jul 2026)</h3>
<table>
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
<tr><td>Neue F&uuml;hrung eingesetzt</td><td>Management</td><td>Offen</td></tr>
<tr><td>Hypercare-Kriterien definiert und erf&uuml;llt</td><td>PO + BO</td><td>Offen</td></tr>
<tr><td>&Uuml;bergang zu Normalbetrieb dokumentiert</td><td>Alle</td><td>Offen</td></tr>
<tr><td>Support-Prozesse definiert (Eskalation, SLAs)</td><td>BSSUPPORT + OPs</td><td>Offen</td></tr>
</table>
"""
html += """
<h2>Phase 2: NORMALBETRIEB + REGULATORIK (Aug &ndash; Nov 2026)</h2>
<ac:structured-macro ac:name="panel"><ac:parameter ac:name="title">Ziel</ac:parameter><ac:rich-text-body>
<p>Vertragliche Pflichten erf&uuml;llen, Abrechnung stabilisieren, Team-Reorg starten</p>
</ac:rich-text-body></ac:structured-macro>
<h3>M4: Service Level Silber Plus (31. Aug 2026) &#9888;</h3>
<table>
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
<tr><td>Netcool-Anbindung produktiv</td><td>OPs</td><td>&#128260; In Arbeit</td></tr>
<tr><td>Monitoring-Konzept abgeschlossen</td><td>OPs + Zero</td><td>&#128260; In Arbeit</td></tr>
<tr><td>Disaster-Recovery-Test 3 bestanden</td><td>OPs</td><td>&#128260; Validierung</td></tr>
<tr><td>Deployment-Automatisierung (Smoketests)</td><td>OPs/DevOps</td><td>&#128260; In Arbeit</td></tr>
<tr><td>Secret-Rotation implementiert</td><td>OPs</td><td>Offen</td></tr>
<tr><td>Kubernetes-Namespaces umgestellt</td><td>OPs</td><td>&#128260; In Arbeit</td></tr>
</table>
<ac:structured-macro ac:name="warning"><ac:rich-text-body>
<p><strong>Risiko:</strong> Vertragsverletzung bei Vers&auml;umnis. Keine Verhandlungsmasse.</p>
</ac:rich-text-body></ac:structured-macro>
<h3>M5: Abrechnung TTT produktionsreif (31. Okt 2026)</h3>
<table>
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
<tr><td>20h-Zug Abrechnung (TTTSOL-2149) entblockt</td><td>CIB</td><td>&#10060; Blocked</td></tr>
<tr><td>Zugtrasse mit Umleitung ohne VT-Wechsel (TTTSOL-2147)</td><td>CIB</td><td>Offen</td></tr>
<tr><td>Abrechnung fremde Infrastruktur (TTTSOL-2148)</td><td>CIB</td><td>Offen</td></tr>
<tr><td>Neue Zugnummer Umleitungsfall (TTTSOL-2035)</td><td>CIB</td><td>&#128260; In Arbeit</td></tr>
<tr><td>VDV-Erweiterung f&uuml;r AC Trasse (strategisch)</td><td>CIB + Zero</td><td>Offen</td></tr>
<tr><td>Alle 41 Abrechnungs-Tickets priorisiert</td><td>CIB PO</td><td>Offen</td></tr>
</table>
<ac:structured-macro ac:name="note"><ac:rich-text-body>
<p>Rechnungsbeginn TTT ist Januar 2027. Bis Oktober m&uuml;ssen kritische F&auml;lle gel&ouml;st sein. Nov/Dez = Buffer f&uuml;r Regressionstests.</p>
</ac:rich-text-body></ac:structured-macro>
<h3>M6: NA&Auml; implizite Annahme produktiv (30. Sep 2026)</h3>
<table>
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
<tr><td>O2CCIB-6931 assigned und implementiert</td><td>CIB</td><td>&#9888; Geschoben</td></tr>
<tr><td>Prozess-Design mit Fachbereich abgestimmt</td><td>CIB BA</td><td>Offen</td></tr>
<tr><td>E2E-Test NA&Auml;-Szenario</td><td>CIB + 404</td><td>Offen</td></tr>
</table>
<h3>M7: Team-Reorg Phase 1 &mdash; OpsDev formiert (30. Sep 2026)</h3>
<table>
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
<tr><td>OpsDev-Team definiert (~6 Personen)</td><td>Management + Leads</td><td>Offen</td></tr>
<tr><td>OpsDev-Lead benannt</td><td>Management</td><td>Offen</td></tr>
<tr><td>Jan Lubenow Wissenstransfer (50% abgeschlossen)</td><td>DevOps</td><td>&#9888; Kritisch</td></tr>
<tr><td>Bug-Triage-Prozess definiert</td><td>OpsDev-Lead</td><td>Offen</td></tr>
<tr><td>OPs-Rotation beendet (feste Zuordnung)</td><td>Management</td><td>Offen</td></tr>
</table>
"""
html += """
<h2>Phase 3: SKALIEREN + REORG (Dez 2026 &ndash; Feb 2027)</h2>
<ac:structured-macro ac:name="panel"><ac:parameter ac:name="title">Ziel</ac:parameter><ac:rich-text-body>
<p>Fahrplanwechsel absichern, Team-Umstellung abschlie&szlig;en, Plattform-Denken etablieren</p>
</ac:rich-text-body></ac:structured-macro>
<h3>M8: Fahrplanwechsel FplJ 28 (Dez 2026) &#9888;</h3>
<table>
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
<tr><td>GelV-Erweiterungen (TTT-Meilenstein)</td><td>CIB + Zero</td><td>Offen</td></tr>
<tr><td>OTN-Vergabe Fpl 2027 (TTTSOL-1683)</td><td>CIB</td><td>&#128260; In Arbeit</td></tr>
<tr><td>ujBau Funktionalit&auml;t</td><td>Alle</td><td>&#128260; In Arbeit</td></tr>
<tr><td>FplJ-28-spezifische Validierungen getestet</td><td>QA</td><td>Offen</td></tr>
<tr><td>Daten-Partitionierung (Performance)</td><td>CIB/Zero</td><td>Offen</td></tr>
</table>
<ac:structured-macro ac:name="warning"><ac:rich-text-body>
<p><strong>Risiko:</strong> Fahrplanwechsel ist harter externer Termin. Kein Verschieben m&ouml;glich.</p>
</ac:rich-text-body></ac:structured-macro>
<h3>M9: Rechnungsbeginn TTT (Jan 2027) &#9888;</h3>
<table>
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
<tr><td>Alle kritischen Abrechnungsf&auml;lle gel&ouml;st (aus M5)</td><td>CIB</td><td>Abh&auml;ngig von M5</td></tr>
<tr><td>Rechnungslauf erfolgreich getestet (Staging)</td><td>CIB + QA</td><td>Offen</td></tr>
<tr><td>Monitoring f&uuml;r Abrechnungsfehler aktiv</td><td>OPs/OpsDev</td><td>Offen</td></tr>
<tr><td>Fallback-Prozess bei Abrechnungsfehlern definiert</td><td>CIB + BO</td><td>Offen</td></tr>
</table>
<h3>M10: Team-Reorg abgeschlossen (31. Jan 2027)</h3>
<table>
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
<tr><td>2 fachliche Teams formiert (&quot;Bestellen&quot; + &quot;Verarbeiten&quot;)</td><td>Management</td><td>Offen</td></tr>
<tr><td>Service-Ownership klar zugeordnet</td><td>Team-Leads</td><td>Offen</td></tr>
<tr><td>PO-Struktur definiert</td><td>Management</td><td>Offen</td></tr>
<tr><td>Wissenstransfer abgeschlossen</td><td>Alle</td><td>Offen</td></tr>
<tr><td>Erste Sprints in neuer Struktur erfolgreich</td><td>Teams</td><td>Offen</td></tr>
<tr><td>Metriken definiert (Bug-Rate, Durchlaufzeit, Deploy-Freq.)</td><td>BO + Leads</td><td>Offen</td></tr>
</table>
<ac:structured-macro ac:name="note"><ac:rich-text-body>
<p>Reorg NACH Fahrplanwechsel abschlie&szlig;en &mdash; nicht w&auml;hrend des kritischen Zeitraums umbauen.</p>
</ac:rich-text-body></ac:structured-macro>
"""
html += """
<h2>Phase 4: INTEGRIEREN + VISION (Feb &ndash; Mai 2027)</h2>
<ac:structured-macro ac:name="panel"><ac:parameter ac:name="title">Ziel</ac:parameter><ac:rich-text-body>
<p>TrassenOrder-Learnings integrieren, Plattform-Architektur vorbereiten, Kundenzufriedenheit steigern</p>
</ac:rich-text-body></ac:structured-macro>
<h3>M11: TrassenOrder-Integration definiert (28. Feb 2027)</h3>
<table>
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
<tr><td>TrassenOrder Livegang evaluiert (Q4 2026)</td><td>TraPo + BO</td><td>Offen</td></tr>
<tr><td>Abgrenzung pathOS/TrassenOrder dokumentiert</td><td>POs + Architektur</td><td>Offen</td></tr>
<tr><td>API-Vertrag zwischen pathOS und TrassenOrder</td><td>CIB/Zero + TraPo</td><td>Offen</td></tr>
<tr><td>Entscheidung: GelV bei pathOS oder TraPo?</td><td>BO + Management</td><td>Offen</td></tr>
<tr><td>&Uuml;bernahme-Szenario TraPo &rarr; pathOS-Team bewertet</td><td>Management</td><td>Offen</td></tr>
</table>
<h3>M12: Plattform-Architektur-Vision (31. M&auml;rz 2027)</h3>
<table>
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
<tr><td>Architektur-Vision &quot;Integriertes Bestellportal&quot; dokumentiert</td><td>Architektur + BO</td><td>Offen</td></tr>
<tr><td>Kernkomponenten als integrierbare Module identifiziert</td><td>Architektur</td><td>Offen</td></tr>
<tr><td>Schnittstellen-Strategie (API-First) definiert</td><td>Architektur + Teams</td><td>Offen</td></tr>
<tr><td>Roadmap f&uuml;r Anlage + Nebenleistungen (n&auml;chste 12M)</td><td>BO + POs</td><td>Offen</td></tr>
<tr><td>TPN-Abl&ouml;sung Zeitplan konkretisiert</td><td>POs + Kunden</td><td>Offen</td></tr>
</table>
<h3>M13: Kundenzufriedenheit messbar verbessert (Mai 2027)</h3>
<table>
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
<tr><td>Bug-Rate Portal unter 15% (von 33%)</td><td>Team &quot;Bestellen&quot;</td><td>Offen</td></tr>
<tr><td>Vorpr&uuml;fung Portal produktiv</td><td>CIB + 404</td><td>Offen</td></tr>
<tr><td>Expertenmodus verf&uuml;gbar</td><td>404</td><td>Offen</td></tr>
<tr><td>&Uuml;bersichtsseite Vorg&auml;nge live</td><td>404</td><td>Offen</td></tr>
<tr><td>Kundenfeedback-Loop etabliert</td><td>PO + UX</td><td>Offen</td></tr>
<tr><td>NPS oder CSAT Baseline gemessen</td><td>PO + BO</td><td>Offen</td></tr>
</table>
"""
html += """
<h2>Kritischer Pfad &amp; Abh&auml;ngigkeiten</h2>
<table>
<tr><th>Pfad</th><th>Abh&auml;ngigkeit</th><th>Konsequenz bei Verz&ouml;gerung</th></tr>
<tr><td>M1 &rarr; M4</td><td>Spring Boot 4 muss fertig sein f&uuml;r SL Silber+ (Security-Compliance)</td><td>Vertragsverletzung</td></tr>
<tr><td>M5 &rarr; M9</td><td>Abrechnung muss bis Okt stehen f&uuml;r Rechnungsbeginn Jan</td><td>Falsche Rechnungen an Kunden</td></tr>
<tr><td>M3 &rarr; M7 &rarr; M10</td><td>F&uuml;hrungswechsel erm&ouml;glicht Reorg, Reorg nach FplJ abschlie&szlig;en</td><td>Produktivit&auml;tsverlust</td></tr>
<tr><td>M8 (extern)</td><td>Fahrplanwechsel ist Fixpunkt &mdash; alles andere drumherum planen</td><td>Fahrplan gef&auml;hrdet</td></tr>
</table>
<h2>Risiko-Matrix</h2>
<table>
<tr><th>Risiko</th><th>Wahrscheinlichkeit</th><th>Impact</th><th>Mitigation</th></tr>
<tr style="background:#ffcdd2;"><td>Abrechnung bleibt blocked (20h-Zug)</td><td>HOCH</td><td>KRITISCH</td><td>Eskalation an TTT-Programm, Workaround</td></tr>
<tr style="background:#ffcdd2;"><td>Bug-Rate 404 steigt weiter</td><td>HOCH</td><td>HOCH</td><td>Entkopplung Zuglaufpunkte priorisieren</td></tr>
<tr><td>SL Silber+ Deadline gerissen</td><td>Mittel</td><td>HOCH</td><td>Fr&uuml;h mit UKA abstimmen, Teilabnahme?</td></tr>
<tr><td>Reorg destabilisiert Teams</td><td>Mittel</td><td>HOCH</td><td>Sukzessiv, nicht Big Bang. Nach FplJ.</td></tr>
<tr><td>Jan Lubenow f&auml;llt aus (SPOF)</td><td>Niedrig</td><td>KRITISCH</td><td>Wissenstransfer ab sofort priorisieren</td></tr>
<tr><td>TrassenOrder scheitert/pivotiert</td><td>Mittel</td><td>MITTEL</td><td>pathOS bleibt autark, GelV intern weiter</td></tr>
<tr><td>Spring Boot 4 nicht rechtzeitig</td><td>Niedrig</td><td>KRITISCH</td><td>Fokus, keine Ablenkung</td></tr>
</table>
<h2>Reorg-Zeitplan (sukzessiv)</h2>
<table>
<tr><th>Zeitraum</th><th>Schritt</th><th>Risiko</th></tr>
<tr><td><strong>Jul 2026</strong></td><td>F&uuml;hrungswechsel, Vision kommunizieren</td><td>Niedrig</td></tr>
<tr><td><strong>Aug-Sep 2026</strong></td><td>OpsDev-Team formieren (aus OPs + DevOps-Teilen)</td><td>Mittel</td></tr>
<tr><td><strong>Okt 2026</strong></td><td>OPs-Rotation beenden, feste Zuordnung</td><td>Niedrig</td></tr>
<tr><td><strong>Nov 2026</strong></td><td>Fachliche Teams vorbereiten (Service-Mapping, Wissenstransfer)</td><td>Mittel</td></tr>
<tr><td><strong>Jan 2027</strong></td><td>Umstellung auf 2 fachliche Teams (&quot;Bestellen&quot; + &quot;Verarbeiten&quot;)</td><td>Hoch</td></tr>
<tr><td><strong>Feb 2027</strong></td><td>Erste Sprints in neuer Struktur, Nachjustierung</td><td>Mittel</td></tr>
<tr><td><strong>M&auml;rz 2027</strong></td><td>TrassenOrder-Team-Integration bewerten</td><td>Mittel</td></tr>
</table>
<h2>Metriken zur Erfolgsmessung</h2>
<table>
<tr><th>Metrik</th><th>Baseline (Mai 2026)</th><th>Ziel Sep 2026</th><th>Ziel Jan 2027</th><th>Ziel Mai 2027</th></tr>
<tr><td>Bug-Rate Portal</td><td>33%</td><td>25%</td><td>20%</td><td>&lt;15%</td></tr>
<tr><td>Bug-Rate Gesamt</td><td>20%</td><td>18%</td><td>15%</td><td>&lt;12%</td></tr>
<tr><td>Deployment-Frequenz</td><td>? (messen!)</td><td>1x/Woche</td><td>2x/Woche</td><td>T&auml;glich m&ouml;glich</td></tr>
<tr><td>Mean Time to Recovery</td><td>? (messen!)</td><td>&lt;4h</td><td>&lt;2h</td><td>&lt;1h</td></tr>
<tr><td>Offene Highest-Tickets</td><td>8+</td><td>&lt;5</td><td>&lt;3</td><td>0</td></tr>
<tr><td>SPOF-Index (&gt;30%)</td><td>2 (Jan, Steven)</td><td>1</td><td>0</td><td>0</td></tr>
<tr><td>Kundenzufriedenheit</td><td>Nicht gemessen</td><td>Baseline</td><td>+10 Pkt</td><td>+20 Pkt</td></tr>
</table>
<h2>Offene Entscheidungen</h2>
<table>
<tr><th>#</th><th>Entscheidung</th><th>Deadline</th><th>Wer</th></tr>
<tr><td>1</td><td>Wer wird neue F&uuml;hrung?</td><td>Jun 2026</td><td>Management</td></tr>
<tr><td>2</td><td>OpsDev-Lead: Intern oder extern?</td><td>Jul 2026</td><td>Management</td></tr>
<tr><td>3</td><td>SV-Monolith: Aufteilen oder einem Team zuordnen?</td><td>Okt 2026</td><td>Architektur + BO</td></tr>
<tr><td>4</td><td>PO-Struktur: 1 pro Team oder &uuml;bergreifend?</td><td>Nov 2026</td><td>BO</td></tr>
<tr><td>5</td><td>TrassenOrder: Eigenes Team oder Integration?</td><td>Feb 2027</td><td>BO + Management</td></tr>
<tr><td>6</td><td>GelV-Ownership: pathOS oder TraPo?</td><td>Feb 2027</td><td>BO</td></tr>
<tr><td>7</td><td>N&auml;chste Dom&auml;ne nach Trasse (Anlage? Nebenleistungen?)</td><td>M&auml;rz 2027</td><td>BO + Strategie</td></tr>
</table>
<hr/>
<p><em>Erstellt im Rahmen des pathOS Portfolio-Audits. Kontakt: Andre Knie</em></p>
"""
# Push to Confluence
def api_request(url, method="GET", data=None):
req = urllib.request.Request(url, method=method)
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("Content-Type", "application/json")
if data:
req.data = json.dumps(data).encode("utf-8")
return json.loads(urllib.request.urlopen(req, context=ctx, timeout=30).read().decode("utf-8"))
# Check if page exists
search_url = f"{BASE}/content?spaceKey={SPACE}&title={urllib.parse.quote(TITLE)}&type=page"
req = urllib.request.Request(search_url)
req.add_header("Authorization", f"Bearer {TOKEN}")
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
search_data = json.loads(resp.read().decode("utf-8"))
if search_data.get("results"):
# Update existing page
page_id = search_data["results"][0]["id"]
ver_url = f"{BASE}/content/{page_id}?expand=version"
req2 = urllib.request.Request(ver_url)
req2.add_header("Authorization", f"Bearer {TOKEN}")
ver_data = json.loads(urllib.request.urlopen(req2, context=ctx, timeout=30).read().decode("utf-8"))
version = ver_data["version"]["number"] + 1
body = json.dumps({
"version": {"number": version},
"title": TITLE,
"type": "page",
"body": {"storage": {"value": html, "representation": "storage"}}
}).encode("utf-8")
req3 = urllib.request.Request(f"{BASE}/content/{page_id}", method="PUT", data=body)
req3.add_header("Authorization", f"Bearer {TOKEN}")
req3.add_header("Content-Type", "application/json")
result = json.loads(urllib.request.urlopen(req3, context=ctx, timeout=30).read().decode("utf-8"))
print(f"Aktualisiert: https://arija-confluence.jaas.service.deutschebahn.com/pages/viewpage.action?pageId={result['id']}")
else:
# Create new page
body = json.dumps({
"type": "page",
"title": TITLE,
"space": {"key": SPACE},
"ancestors": [{"id": PARENT}],
"body": {"storage": {"value": html, "representation": "storage"}}
}).encode("utf-8")
req3 = urllib.request.Request(f"{BASE}/content", method="POST", data=body)
req3.add_header("Authorization", f"Bearer {TOKEN}")
req3.add_header("Content-Type", "application/json")
result = json.loads(urllib.request.urlopen(req3, context=ctx, timeout=30).read().decode("utf-8"))
print(f"Erstellt: https://arija-confluence.jaas.service.deutschebahn.com/pages/viewpage.action?pageId={result['id']}")
@@ -0,0 +1,295 @@
<#
.SYNOPSIS
Pusht die Personal- und Bug-Analyse als Confluence-Seite
.USAGE
powershell -ExecutionPolicy Bypass -File ".\project-audit\scripts\confluence-push-personal.ps1"
#>
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
$SpaceKey = "BES"
$ParentPageId = "581013135"
$ApiBase = "$ConfluenceUrl/rest/api"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "============================================================"
Write-Host " Personal- und Bug-Analyse -> Confluence"
Write-Host "============================================================"
$PAT = Read-Host "Confluence PAT"
$Headers = @{ "Authorization" = "Bearer $PAT"; "Content-Type" = "application/json" }
function Create-Or-Update-Page {
param([string]$Title, [string]$HtmlBody, [string]$ParentId)
# Umlaute automatisch konvertieren
$HtmlBody = Fix-Umlauts $HtmlBody
$SearchUri = "$ApiBase/content?spaceKey=$SpaceKey&title=$([Uri]::EscapeDataString($Title))&type=page"
try {
$SearchResp = Invoke-WebRequest -Uri $SearchUri -Headers $Headers -UseBasicParsing -ErrorAction Stop
$SearchData = $SearchResp.Content | ConvertFrom-Json
} catch {
Write-Host " FEHLER bei Suche: $($_.Exception.Message)" -ForegroundColor Red
return $null
}
if ($SearchData.results -and $SearchData.results.Count -gt 0) {
$PageId = $SearchData.results[0].id
try {
$VerResp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId`?expand=version,body.storage" -Headers $Headers -UseBasicParsing -ErrorAction Stop
$VerData = $VerResp.Content | ConvertFrom-Json
$Version = $VerData.version.number + 1
} catch { $Version = $SearchData.results[0].version.number + 1 }
Write-Host " Aktualisiere (ID: $PageId, Version: $Version)..." -NoNewline
$Body = @{
version = @{ number = $Version }
title = $Title
type = "page"
body = @{ storage = @{ value = $HtmlBody; representation = "storage" } }
} | ConvertTo-Json -Depth 10
try {
$Resp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId" -Method Put -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($Body)) -UseBasicParsing -ErrorAction Stop
Write-Host " OK" -ForegroundColor Green
return ($Resp.Content | ConvertFrom-Json).id
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
return $null
}
} else {
Write-Host " Erstelle neue Seite..." -NoNewline
$Body = @{
type = "page"
title = $Title
space = @{ key = $SpaceKey }
ancestors = @(@{ id = $ParentId })
body = @{ storage = @{ value = $HtmlBody; representation = "storage" } }
} | ConvertTo-Json -Depth 10
try {
$Resp = Invoke-WebRequest -Uri "$ApiBase/content" -Method Post -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($Body)) -UseBasicParsing -ErrorAction Stop
Write-Host " OK" -ForegroundColor Green
return ($Resp.Content | ConvertFrom-Json).id
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
return $null
}
}
}
# ============================================================
# SEITE: Personal- und Bug-Analyse
# ============================================================
Write-Host ""
Write-Host ">> Seite 22: Personal- und Bug-Analyse" -ForegroundColor Yellow
$PersonalHtml = @"
<h2>Personal- und Bug-Analyse pathOS</h2>
<p><strong>Stand:</strong> 2026-04-27 | <strong>Quellen:</strong> Jira (90d + 12M), Rollen-Mapping (BO-Input), Tenure-Daten</p>
<ac:structured-macro ac:name="info"><ac:rich-text-body>
<p>Diese Analyse korreliert Personaldaten (Rollen, Tenure, Aktivitaet) mit Bug-Raten und Qualitaetskennzahlen pro Team und Entwickler.</p>
</ac:rich-text-body></ac:structured-macro>
<h3>Team-Gesundheit im Ueberblick</h3>
<table>
<tr><th>Team</th><th>Personen</th><th>Kern-Devs</th><th>Bug-Rate (90d)</th><th>Bug-Rate (12M)</th><th>Trend</th><th>Bewertung</th></tr>
<tr style="background:#f8cecc;"><td><strong>Team 404</strong></td><td>10</td><td>7</td><td>33%</td><td>25%</td><td>&#8593; Steigend</td><td>KRITISCH</td></tr>
<tr style="background:#d5e8d4;"><td><strong>Team CIB</strong></td><td>13</td><td>6</td><td>20%</td><td>23%</td><td>&#8595; Sinkend</td><td>GUT</td></tr>
<tr style="background:#d5e8d4;"><td><strong>Team Zero</strong></td><td>9</td><td>6</td><td>15%</td><td>15%</td><td>&#8594; Stabil</td><td>GUT</td></tr>
<tr style="background:#fff2cc;"><td><strong>OPs Squad</strong></td><td>7+ rot.</td><td>2 fest</td><td>31%</td><td>27%</td><td>&#8595; Sinkend</td><td>BEOBACHTEN</td></tr>
<tr style="background:#fff2cc;"><td><strong>DevOps</strong></td><td>7</td><td>3</td><td>9%</td><td>7%</td><td>&#8594; Stabil</td><td>OK (SPOF)</td></tr>
</table>
<h3>Team 404 (Portal) &#8212; KRITISCH</h3>
<ac:structured-macro ac:name="warning"><ac:rich-text-body>
<p>Hoechste Bug-Rate (33%), steigend seit Go-Live. Portal ist kundenseitig &#8212; Bugs werden direkt von EVUs gemeldet.</p>
</ac:rich-text-body></ac:structured-macro>
<table>
<tr><th>Person</th><th>Rolle</th><th>Seit</th><th>Issues (90d)</th><th>Bug-Anteil</th><th>Done%</th><th>Bewertung</th></tr>
<tr style="background:#d5e8d4;"><td>Ana Cvitkovic</td><td>Business Engineer</td><td>2021-08</td><td>78</td><td>niedrig</td><td>hoch</td><td>TOP &#8212; Anker des Teams</td></tr>
<tr><td>Jasmin Keskin</td><td>Frontend Dev</td><td>2021-08</td><td>45</td><td>42%</td><td>mittel</td><td>Bug-anfaellig</td></tr>
<tr style="background:#f8cecc;"><td>Emmanuel Kontcheu Tagne</td><td>Frontend Dev</td><td>2021-08</td><td>42</td><td>69%</td><td>mittel</td><td>KRITISCH</td></tr>
<tr><td>Diego Da Costa Souza</td><td>Frontend Dev</td><td>2021-09</td><td>41</td><td>mittel</td><td>mittel</td><td>Spring Boot 4 Portal</td></tr>
<tr style="background:#f8cecc;"><td>Leon Hoerpel</td><td>Dev</td><td>?</td><td>35</td><td>71%</td><td>mittel</td><td>KRITISCH</td></tr>
<tr><td>Dominik Ruecker</td><td>Backend Dev</td><td>?</td><td>31</td><td>mittel</td><td>mittel</td><td></td></tr>
<tr style="background:#d5e8d4;"><td>Annette Halbhuber</td><td>Lead Dev</td><td>2021-09</td><td>26</td><td>niedrig</td><td>hoch</td><td>Stabil, Lead</td></tr>
<tr style="background:#d5e8d4;"><td>Simon Reitinger</td><td>Dev</td><td>2025-11</td><td>15</td><td>0%</td><td>100%</td><td>Exzellenter Start</td></tr>
<tr><td>Marcel Hufgard</td><td>PO</td><td>2021-12</td><td>8</td><td>&#8212;</td><td>&#8212;</td><td>Product Owner</td></tr>
<tr style="background:#fff2cc;"><td>Luca Caracciolo</td><td>QA</td><td>2021-09</td><td>5</td><td>&#8212;</td><td>&#8212;</td><td>Wenig aktiv</td></tr>
</table>
<p><strong>Kernproblem:</strong> Emmanuel (69% Bugs) und Leon (71% Bugs) arbeiten beide im Frontend. Das Portal-Frontend hat ein systematisches Qualitaetsproblem. QA (Luca) ist wenig aktiv &#8212; Testabdeckung unzureichend.</p>
<h3>Team CIB (Prozesse/Backend) &#8212; Erfolgsgeschichte</h3>
<ac:structured-macro ac:name="tip"><ac:rich-text-body>
<p>Bug-Rate von 36% (Jan) auf 9% (Apr) gesunken. Backend stabilisiert sich nach Go-Live.</p>
</ac:rich-text-body></ac:structured-macro>
<table>
<tr><th>Person</th><th>Rolle</th><th>Seit</th><th>Issues (90d)</th><th>Bug-Anteil</th><th>Done%</th><th>Bewertung</th></tr>
<tr style="background:#d5e8d4;"><td>Bishara Jaser</td><td>Dev</td><td>2022-08</td><td>73</td><td>niedrig</td><td>93%</td><td>TOP &#8212; Leistungstraeger</td></tr>
<tr style="background:#d5e8d4;"><td>Saurav Kumar</td><td>Dev</td><td>2022-01</td><td>48</td><td>8%</td><td>94%</td><td>TOP &#8212; niedrigste Bug-Rate</td></tr>
<tr style="background:#d5e8d4;"><td>Jonas Koehler</td><td>Dev</td><td>?</td><td>34</td><td>niedrig</td><td>100%</td><td>Exzellent</td></tr>
<tr><td>Dong-Won Han</td><td>Dev</td><td>2021-09</td><td>20</td><td>mittel</td><td>mittel</td><td>Veteran</td></tr>
<tr><td>Hans-Henning Ramberger</td><td>Dev</td><td>2024-05</td><td>16</td><td>mittel</td><td>mittel</td><td>Auch OPs</td></tr>
<tr><td>Vasileios Dimitriadis</td><td>Dev</td><td>2023-11</td><td>11</td><td>mittel</td><td>mittel</td><td>Wenig aktiv</td></tr>
<tr><td>Christian Meins</td><td>PO</td><td>2021-10</td><td>9</td><td>&#8212;</td><td>&#8212;</td><td>Product Owner</td></tr>
<tr><td>Harry Braun</td><td>Dev</td><td>2023-01</td><td>5</td><td>mittel</td><td>mittel</td><td>SB4 SV</td></tr>
<tr><td>Olaf Becken</td><td>Business Engineer</td><td>2024-05</td><td>5</td><td>&#8212;</td><td>&#8212;</td><td></td></tr>
<tr style="background:#fff2cc;"><td>Alexander Petioky</td><td>Dev</td><td>2021-10</td><td>1</td><td>&#8212;</td><td>&#8212;</td><td>Kaum aktiv</td></tr>
</table>
<p><strong>Kern-Trio:</strong> Bishara + Saurav + Jonas (155 Issues, &gt;93% Done). Deren Arbeitsweise sollte als Vorbild fuer andere Teams dienen.</p>
<h3>Team Zero (TAF/TAP) &#8212; Stabil</h3>
<ac:structured-macro ac:name="tip"><ac:rich-text-body>
<p>Niedrigste Bug-Rate (15%), stabiler Output. Beste SonarQube-Qualitaet (4.2 Smells/1K, 91.8% Coverage).</p>
</ac:rich-text-body></ac:structured-macro>
<table>
<tr><th>Person</th><th>Rolle</th><th>Seit</th><th>Issues (90d)</th><th>Bug-Anteil</th><th>Bewertung</th></tr>
<tr style="background:#d5e8d4;"><td>Steven Meixner</td><td>Dev</td><td>2022-07</td><td>64 + 27 OPs</td><td>niedrig</td><td>TOP &#8212; Allrounder (SPOF!)</td></tr>
<tr style="background:#d5e8d4;"><td>Bing Shi</td><td>Dev</td><td>2022-04</td><td>63</td><td>niedrig</td><td>Stark</td></tr>
<tr style="background:#d5e8d4;"><td>Frank Lemke</td><td>Dev</td><td>2022-12</td><td>49</td><td>niedrig</td><td>Auch OPs</td></tr>
<tr><td>Kathrin Schleich</td><td>Dev</td><td>2021-09</td><td>27</td><td>niedrig</td><td>Auch OPs</td></tr>
<tr style="background:#fff2cc;"><td>Michael Weisberg</td><td>Test</td><td>2024-11</td><td>23</td><td>&#8212;</td><td>52% offen</td></tr>
<tr><td>Bernd Klebl</td><td>PO</td><td>2021-09</td><td>16</td><td>&#8212;</td><td>Auch OPs</td></tr>
<tr style="background:#f8cecc;"><td>Norbert Maurer</td><td>Dev</td><td>2021-11</td><td>12</td><td>&#8212;</td><td>83% offen &#8212; ausgeschieden?</td></tr>
</table>
<h3>DevOps &#8212; SPOF-Risiko</h3>
<ac:structured-macro ac:name="warning"><ac:rich-text-body>
<p><strong>Jan Lubenow = 39% aller DevOps-Issues</strong> &#8212; kritischster Single Point of Failure im gesamten Programm.</p>
</ac:rich-text-body></ac:structured-macro>
<table>
<tr><th>Person</th><th>Rolle</th><th>Seit</th><th>Issues (90d)</th><th>Anteil</th></tr>
<tr style="background:#f8cecc;"><td><strong>Jan Lubenow</strong></td><td>Lead Dev</td><td>2022-09</td><td>114</td><td>39%</td></tr>
<tr><td>David Steinkopff</td><td>Dev</td><td>2023-03</td><td>18</td><td>6%</td></tr>
<tr><td>Henrik Scholl</td><td>Dev</td><td>2024-10</td><td>10</td><td>3%</td></tr>
<tr><td>Christian Prause</td><td>Dev</td><td>?</td><td>7</td><td>2%</td></tr>
<tr><td>Patrick Lewandowski</td><td>Dev</td><td>?</td><td>6</td><td>2%</td></tr>
<tr><td>Michael Mh Jahn</td><td>Dev</td><td>?</td><td>6</td><td>2%</td></tr>
<tr><td>Sebastian Goendoer</td><td>Dev</td><td>?</td><td>3</td><td>1%</td></tr>
</table>
<h3>Tenure vs. Bug-Rate &#8212; Korrelation</h3>
<table>
<tr><th>Team</th><th>Hypothese: Laenger dabei = weniger Bugs?</th><th>Ergebnis</th></tr>
<tr><td>Team 404</td><td>Emmanuel (56 Monate, 69% Bugs), Simon (5 Monate, 0% Bugs)</td><td><strong>WIDERLEGT</strong> &#8212; Systematisches Problem</td></tr>
<tr><td>Team CIB</td><td>Saurav (51 Monate, 8% Bugs), Bishara (44 Monate, niedrig)</td><td><strong>BESTAETIGT</strong> &#8212; Erfahrung + gute Prozesse</td></tr>
<tr><td>Team Zero</td><td>Alle Veteranen niedrige Bug-Rate, beste SonarQube-Werte</td><td><strong>BESTAETIGT</strong> &#8212; Stabiles Team + Praktiken</td></tr>
</table>
<p><strong>Fazit:</strong> Tenure allein erklaert die Bug-Rate nicht. Team 404 hat ein systematisches Frontend-Qualitaetsproblem, das nicht durch Erfahrung geloest wird. CIB und Zero zeigen, dass gute Prozesse + Erfahrung zusammen wirken.</p>
<h3>Risiko-Matrix Personal</h3>
<h4>KRITISCH (sofort handeln)</h4>
<table>
<tr><th>Risiko</th><th>Person(en)</th><th>Impact</th><th>Massnahme</th></tr>
<tr style="background:#f8cecc;"><td>SPOF DevOps</td><td>Jan Lubenow (39%)</td><td>Deployment, CI/CD, Infra</td><td>Wissenstransfer, Dokumentation</td></tr>
<tr style="background:#f8cecc;"><td>SPOF Zero/OPs</td><td>Steven Meixner (91 Issues)</td><td>TAF/TAP + Infrastruktur</td><td>Entlastung, Backup aufbauen</td></tr>
<tr style="background:#f8cecc;"><td>Bug-Verursacher 404</td><td>Emmanuel (69%), Leon (71%)</td><td>Portal-Qualitaet</td><td>Code-Reviews, Pair Programming</td></tr>
</table>
<h4>HOCH (kurzfristig)</h4>
<table>
<tr><th>Risiko</th><th>Person(en)</th><th>Impact</th><th>Massnahme</th></tr>
<tr style="background:#fff2cc;"><td>QA-Engpass 404</td><td>Luca Caracciolo (5 Issues)</td><td>Testabdeckung Portal</td><td>Aktivieren oder ersetzen</td></tr>
<tr style="background:#fff2cc;"><td>Test-Backlog Zero</td><td>Michael Weisberg (52% offen)</td><td>Testabdeckung TAF/TAP</td><td>Priorisierung</td></tr>
<tr style="background:#fff2cc;"><td>Inaktive Mitglieder</td><td>Norbert Maurer, Alexander Petioky</td><td>Offene Issues, Wissen</td><td>Status klaeren</td></tr>
</table>
<h3>Top 5 Massnahmen (priorisiert)</h3>
<ol>
<li><strong>Jan Lubenow entlasten</strong> &#8212; Wissenstransfer auf David Steinkopff + Henrik Scholl. DevOps-Runbook beschleunigen. Ziel: Kein Einzelner &gt;25% der Issues.</li>
<li><strong>Portal-Qualitaet steigern (404)</strong> &#8212; Code-Reviews fuer Emmanuel/Leon verstaerken. Frontend-Testautomatisierung ausbauen. QA aktivieren. Fehlermeldungen-Konzept umsetzen.</li>
<li><strong>Steven Meixner entlasten</strong> &#8212; OPs-Rotation auf CIB/404 ausweiten. Steven soll sich auf Zero-Kernarbeit konzentrieren.</li>
<li><strong>Inaktive klaeren</strong> &#8212; Norbert Maurer (Zero), Alexander Petioky (CIB), Sebastian Goendoer (OPs): Status klaeren, offene Issues umverteilen.</li>
<li><strong>CIB Best Practices teilen</strong> &#8212; Bishara/Saurav/Jonas als Mentoren. Deren Arbeitsweise (&gt;93% Done, &lt;10% Bugs) als Vorbild fuer 404.</li>
</ol>
"@
# Umlaute: Nutze gleiche Funktion wie confluence-push.ps1
# Schritt 1: Englische Woerter schuetzen, Schritt 2: Ersetzen, Schritt 3: Schutz aufheben
function Fix-Umlauts($html) {
if ($null -eq $html -or $html -eq "") { return $html }
$Protect = @(
'Israel','Michael','Raphael','Emmanuel','Manuel','Daniel','Samuel',
'Maurer','Enabler','enabler','Container','container','Cluster','cluster',
'User','user','Tracing','Staging','Logging','Alerting','Monitoring',
'Deployment','deployment','Security','security','Recovery','Discovery',
'Delivery','Pipeline','pipeline','Release','release',
'Performance','performance','Interface','interface',
'Service','service','Feature','feature','Issue','issue',
'value','true','blue','Due','due','Queue','queue',
'Unique','unique','Continue','continue',
'Funnel','Tunnel','tunnel','Buildpacks','Dockerfile',
'OpenSearch','Gherkin','Kubernetes','Helmfile','Renovate',
'Operate','Template','template','Tasklist',
'Smoke','smoke','Analyse','analyse','Analysis',
'Baseline','baseline','Namespace','namespace',
'Codebase','Database','database','Rescue','rescue'
)
foreach ($w in $Protect) { $html = $html.Replace($w, "PROTECT_$w") }
# Namen
$html = $html.Replace('Hoerpel', 'H&ouml;rpel')
$html = $html.Replace('Ruecker', 'R&uuml;cker')
$html = $html.Replace('Koehler', 'K&ouml;hler')
$html = $html.Replace('Goendoer', 'G&ouml;nd&ouml;r')
# ue
$html = $html.Replace('Ueber', '&Uuml;ber')
$html = $html.Replace('ueber', '&uuml;ber')
$html = $html.Replace('Pruef', 'Pr&uuml;f'); $html = $html.Replace('pruef', 'pr&uuml;f')
$html = $html.Replace('fuehrung', 'f&uuml;hrung'); $html = $html.Replace('Fuehrung', 'F&uuml;hrung')
$html = $html.Replace('Verfueg', 'Verf&uuml;g'); $html = $html.Replace('verfueg', 'verf&uuml;g')
$html = $html.Replace('Verschluess', 'Verschl&uuml;ss'); $html = $html.Replace('verschluess', 'verschl&uuml;ss')
$html = $html.Replace('Schluess', 'Schl&uuml;ss'); $html = $html.Replace('schluess', 'schl&uuml;ss')
$html = $html.Replace('Verknuepf', 'Verkn&uuml;pf'); $html = $html.Replace('verknuepf', 'verkn&uuml;pf')
$html = $html.Replace('Unterstuetz', 'Unterst&uuml;tz'); $html = $html.Replace('unterstuetz', 'unterst&uuml;tz')
$html = $html.Replace('Luecke', 'L&uuml;cke'); $html = $html.Replace('luecke', 'l&uuml;cke')
$html = $html.Replace('Rueck', 'R&uuml;ck'); $html = $html.Replace('rueck', 'r&uuml;ck')
$html = $html.Replace('Frueh', 'Fr&uuml;h'); $html = $html.Replace('frueh', 'fr&uuml;h')
$html = $html.Replace('Nuetzlich', 'N&uuml;tzlich'); $html = $html.Replace('nuetzlich', 'n&uuml;tzlich')
$html = $html.Replace('Abkuerz', 'Abk&uuml;rz'); $html = $html.Replace('abkuerz', 'abk&uuml;rz')
$html = $html.Replace('wuerde', 'w&uuml;rde'); $html = $html.Replace('Wuerde', 'W&uuml;rde')
$html = $html.Replace('muessen', 'm&uuml;ssen'); $html = $html.Replace('Muessen', 'M&uuml;ssen')
$html = $html.Replace('fuer ', 'f&uuml;r '); $html = $html.Replace('Fuer ', 'F&uuml;r ')
# oe
$html = $html.Replace('Loesung', 'L&ouml;sung'); $html = $html.Replace('loesung', 'l&ouml;sung')
$html = $html.Replace('Erhoeh', 'Erh&ouml;h'); $html = $html.Replace('erhoeh', 'erh&ouml;h')
$html = $html.Replace('noetig', 'n&ouml;tig'); $html = $html.Replace('Noetig', 'N&ouml;tig')
$html = $html.Replace('moeglich', 'm&ouml;glich'); $html = $html.Replace('Moeglich', 'M&ouml;glich')
$html = $html.Replace('geloest', 'gel&ouml;st')
$html = $html.Replace('hoechst', 'h&ouml;chst'); $html = $html.Replace('Hoechst', 'H&ouml;chst')
$html = $html.Replace('koennen', 'k&ouml;nnen'); $html = $html.Replace('Koennen', 'K&ouml;nnen')
$html = $html.Replace('Zugehoer', 'Zugeh&ouml;r'); $html = $html.Replace('zugehoer', 'zugeh&ouml;r')
$html = $html.Replace('gehoer', 'geh&ouml;r')
# ae
$html = $html.Replace('Aenderung', '&Auml;nderung'); $html = $html.Replace('aenderung', '&auml;nderung')
$html = $html.Replace('anfaellig', 'anf&auml;llig'); $html = $html.Replace('faellig', 'f&auml;llig')
$html = $html.Replace('faehig', 'f&auml;hig')
$html = $html.Replace('traeger', 'tr&auml;ger'); $html = $html.Replace('Traeger', 'Tr&auml;ger')
$html = $html.Replace('laenger', 'l&auml;nger'); $html = $html.Replace('Laenger', 'L&auml;nger')
$html = $html.Replace('erklaer', 'erkl&auml;r')
$html = $html.Replace('klaer', 'kl&auml;r'); $html = $html.Replace('Klaer', 'Kl&auml;r')
$html = $html.Replace('verstaerk', 'verst&auml;rk')
$html = $html.Replace('Verstaend', 'Verst&auml;nd'); $html = $html.Replace('verstaend', 'verst&auml;nd')
$html = $html.Replace('Abhaengig', 'Abh&auml;ngig'); $html = $html.Replace('abhaengig', 'abh&auml;ngig')
$html = $html.Replace('haeufig', 'h&auml;ufig'); $html = $html.Replace('Haeufig', 'H&auml;ufig')
$html = $html.Replace('laeuft', 'l&auml;uft'); $html = $html.Replace('Laeuft', 'L&auml;uft')
$html = $html.Replace('waere', 'w&auml;re'); $html = $html.Replace('Waere', 'W&auml;re')
$html = $html.Replace('schaerfen', 'sch&auml;rfen'); $html = $html.Replace('Schaerfen', 'Sch&auml;rfen')
$html = $html.Replace('aufraeumen', 'aufr&auml;umen'); $html = $html.Replace('Aufraeumen', 'Aufr&auml;umen')
$html = $html.Replace('Verlaenger', 'Verl&auml;nger'); $html = $html.Replace('verlaenger', 'verl&auml;nger')
$html = $html.Replace('Naechst', 'N&auml;chst'); $html = $html.Replace('naechst', 'n&auml;chst')
$html = $html.Replace('Massnahme', 'Ma&szlig;nahme'); $html = $html.Replace('massnahme', 'ma&szlig;nahme')
$html = $html.Replace('Groesse', 'Gr&ouml;&szlig;e'); $html = $html.Replace('groesse', 'gr&ouml;&szlig;e')
$html = $html.Replace('gleichmaessig', 'gleichm&auml;&szlig;ig')
$html = $html -replace '([a-z])taet\b', '$1t&auml;t'
# Schutz aufheben
$html = $html.Replace('PROTECT_', '')
return $html
}
$PageId = Create-Or-Update-Page -Title "22. Personal- und Bug-Analyse" -HtmlBody $PersonalHtml -ParentId $ParentPageId
Write-Host ""
if ($PageId) {
Write-Host "FERTIG! $ConfluenceUrl/pages/viewpage.action?pageId=$PageId" -ForegroundColor Green
} else {
Write-Host "FEHLER beim Erstellen der Seite" -ForegroundColor Red
}
@@ -0,0 +1,189 @@
<#
.SYNOPSIS
Pusht die Roadmap-Tabelle als Confluence-Seite
#>
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
$SpaceKey = "BES"
$ParentPageId = "581013135"
$ApiBase = "$ConfluenceUrl/rest/api"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$SecretsPath = Join-Path $ScriptDir "..\.secrets"
$PAT = $null
if (Test-Path $SecretsPath) {
Get-Content $SecretsPath | ForEach-Object {
if ($_ -match "^CONFLUENCE_TOKEN=(.+)$") {
$val = $Matches[1].Trim()
if ($val -ne "HIER_TOKEN_EINTRAGEN") { $PAT = $val }
}
}
}
if (-not $PAT) { $PAT = Read-Host "Confluence PAT" }
$Headers = @{ "Authorization" = "Bearer $PAT"; "Content-Type" = "application/json" }
$Title = "24. Roadmap-Tabelle (alle Themen)"
Write-Host ">> Push: $Title" -ForegroundColor Yellow
$Html = @"
<h2>Roadmap-Tabelle &#8212; Alle Themen nach Team</h2>
<p><strong>Stand:</strong> $(Get-Date -Format 'yyyy-MM-dd') | 77 Themen | Quellen: Technische Roadmap, PO-Input, BO-Input, Jira TTTSol, Risiko-Radar</p>
<p><strong>Legende Prio:</strong> MUSS = regulatorisch/vertraglich | HOCH = geschaeftskritisch | MITTEL = wichtig | NIEDRIG = nice-to-have</p>
<p><strong>Legende Dauer:</strong> S = Sprint (2W) | M = Monat | Q = Quartal | L = Langfristig (&gt;Q)</p>
<h3>Kritischer Pfad (harte Deadlines)</h3>
<table>
<tr style="background:#f8cecc;"><th>Deadline</th><th>Thema</th><th>Team</th><th>Konsequenz bei Versaeumnis</th></tr>
<tr style="background:#f8cecc;"><td><strong>Jun 2026</strong></td><td>Spring Boot 4</td><td>Alle</td><td>Kein Security-Support mehr</td></tr>
<tr style="background:#fff2cc;"><td><strong>Aug 2026</strong></td><td>SL Silber Plus</td><td>OPs</td><td>Vertragsverletzung</td></tr>
<tr style="background:#fff2cc;"><td><strong>Dez 2026</strong></td><td>FplJ 28</td><td>Alle</td><td>Fahrplanwechsel gefaehrdet</td></tr>
<tr><td>Laufend</td><td>Abrechnung TTT</td><td>CIB</td><td>Falsche Rechnungen an Kunden</td></tr>
<tr><td>Laufend</td><td>NAÄ implizite Annahme</td><td>CIB</td><td>Kundenversprechen gebrochen</td></tr>
</table>
<h3>Team CIB (17 Themen)</h3>
<table>
<tr><th>#</th><th>Thema</th><th>Kurzfassung</th><th>Prio</th><th>Dauer</th><th>Deadline</th><th>Status</th></tr>
<tr style="background:#f8cecc;"><td>C1</td><td><strong>Implizite Annahme NAÄ</strong></td><td>Kundenversprechen: Standard-Prozess fuer NAÄ. O2CCIB-6931 unassigned.</td><td>MUSS</td><td>M</td><td>PI 41</td><td>&#9888; Geschoben</td></tr>
<tr style="background:#f8cecc;"><td>C2</td><td><strong>Spring Boot 4 (SV, AV, IFP)</strong></td><td>EOL 3.5 Juni 2026. SV (Saurav), Archivierung (Hans-Henning), IFP aktiv.</td><td>MUSS</td><td>M</td><td><strong>Jun 2026</strong></td><td>&#128260; In Arbeit</td></tr>
<tr style="background:#f8cecc;"><td>C3</td><td><strong>Abrechnung TTT/AC</strong></td><td>41 offene Tickets, 8 Highest. Grundrisiko: Nicht korrekt abrechnen.</td><td>MUSS</td><td>Q</td><td>Laufend</td><td>&#128260; Taskforce</td></tr>
<tr style="background:#f8cecc;"><td>C4</td><td><strong>20h Zug Abrechnung</strong></td><td>TTTSOL-2149 BLOCKED. Loesung funktioniert nicht.</td><td>MUSS</td><td>M</td><td>PI 41</td><td>&#10060; Blocked</td></tr>
<tr style="background:#fff2cc;"><td>C5</td><td>Vertragskorrektur rueckwirkend</td><td>TTTSOL-1956 Highest. Konzernreporting.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>&#128260;</td></tr>
<tr style="background:#fff2cc;"><td>C6</td><td>NEP2 Funktionalitaet</td><td>Aktueller TTT-Meilenstein. Laeuft stabil.</td><td>HOCH</td><td>Q</td><td>Laufend</td><td>&#128260;</td></tr>
<tr style="background:#fff2cc;"><td>C7</td><td>Aenderungswesen finalisieren</td><td>Parallele Aenderungsbestellungen validieren.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
<tr style="background:#fff2cc;"><td>C8</td><td>Vorpruefung Portal</td><td>Validierung VOR Absenden. Verhindert Haengenbleiben.</td><td>HOCH</td><td>M</td><td>PI 41/42</td><td>Offen</td></tr>
<tr style="background:#fff2cc;"><td>C9</td><td>Click&amp;Ride Anbindung</td><td>ADR-72. SUBP arbeitet an Teilen.</td><td>HOCH</td><td>Q</td><td>PI 41</td><td>Teilweise</td></tr>
<tr><td>C10</td><td>Camunda 8.9</td><td>Support-Zeitraeume beachten.</td><td>MITTEL</td><td>S</td><td>PI 41</td><td>Offen</td></tr>
<tr><td>C11</td><td>Full-Table-Scans AV</td><td>Performance-Problem.</td><td>MITTEL</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
<tr><td>C12</td><td>RouteUpdate-Prozess</td><td>Noch nicht implementiert. Nachlieferung.</td><td>MITTEL</td><td>Q</td><td>PI 42</td><td>Offen</td></tr>
<tr><td>C13</td><td>&#167;22 ERegG</td><td>Eintritt Drittunternehmen in Vertrag.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
<tr><td>C14</td><td>Leistungsverweigerung</td><td>Durchsetzung Stufe 2.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
<tr><td>C15</td><td>Rahmenvertraege aufraeumen</td><td>Reduziert Pflegeaufwand.</td><td>NIEDRIG</td><td>S</td><td>&#8212;</td><td>Offen</td></tr>
<tr><td>C16</td><td>Mehrfach-Beanstandung Fix</td><td>Nur erste wird weitergeleitet.</td><td>NIEDRIG</td><td>S</td><td>&#8212;</td><td>Offen</td></tr>
<tr><td>C17</td><td>SV-Smells abbauen</td><td>2214 Smells. Sprint-weise.</td><td>NIEDRIG</td><td>L</td><td>Laufend</td><td>Offen</td></tr>
</table>
"@
$Html += @"
<h3>Team 404 (17 Themen)</h3>
<table>
<tr><th>#</th><th>Thema</th><th>Kurzfassung</th><th>Prio</th><th>Dauer</th><th>Deadline</th><th>Status</th></tr>
<tr style="background:#fff2cc;"><td>P1</td><td><strong>Portal-Bug-Rate senken</strong></td><td>33% Bug-Rate. Emmanuel 69%, Leon 71%. Systematisch.</td><td>HOCH</td><td>L</td><td>Laufend</td><td>&#9888; Kritisch</td></tr>
<tr style="background:#fff2cc;"><td>P2</td><td><strong>Entkopplung Zuglaufpunkte</strong></td><td>Vererbungslogik entfernen &#8212; Hauptquelle vieler Bugs.</td><td>HOCH</td><td>Q</td><td>PI 41/42</td><td>Offen</td></tr>
<tr style="background:#fff2cc;"><td>P3</td><td>Fehlermeldungen-Konzept</td><td>Verstaendliche Meldungen. BO-Prioritaet.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
<tr style="background:#fff2cc;"><td>P4</td><td>Uebersichtsseite Vorgaenge</td><td>Gruppiert nach RouteID.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
<tr style="background:#fff2cc;"><td>P5</td><td>E2E-Tests ausbauen</td><td>Playwright/Cypress. Coverage gut, Testqualitaet nicht.</td><td>HOCH</td><td>L</td><td>Laufend</td><td>Offen</td></tr>
<tr style="background:#fff2cc;"><td>P6</td><td>Testumgebung mit TPN</td><td>Dauerhaft verfuegbar.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
<tr style="background:#fff2cc;"><td>P7</td><td>Vorab-Validierung (BEP)</td><td>Zusammen mit CIB C8.</td><td>HOCH</td><td>M</td><td>PI 41/42</td><td>Offen</td></tr>
<tr><td>P8</td><td>Benachrichtigungen E-Mail</td><td>Bei NAÄ oder Angebotseingang.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
<tr><td>P9</td><td>Expertenmodus</td><td>Kompakte Maske fuer Erfahrene.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
<tr><td>P10</td><td>Massenkopie Taktbestellungen</td><td>Mehrere Takte kopieren.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
<tr><td>P11</td><td>Suchfunktion</td><td>Leistungsfaehige Suche.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
<tr><td>P12</td><td>Kalenderfunktion</td><td>Kunden tun sich schwer.</td><td>MITTEL</td><td>S</td><td>PI 42</td><td>Offen</td></tr>
<tr><td>P13</td><td>Verknuepfungslogik</td><td>Planned/RelatedPlanned IDs.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
<tr><td>P14</td><td>Vorlagen ueberarbeiten</td><td>Fehleranfaellig. Konzept noetig.</td><td>MITTEL</td><td>M</td><td>PI 43</td><td>Offen</td></tr>
<tr><td>P15</td><td>Auslands-/NE-Strecken</td><td>Hilfe beim Anlegen.</td><td>NIEDRIG</td><td>M</td><td>&#8212;</td><td>Offen</td></tr>
<tr><td>P16</td><td>TPN-Import optimieren</td><td>Zu komplex.</td><td>NIEDRIG</td><td>Q</td><td>&#8212;</td><td>Offen</td></tr>
<tr><td>P17</td><td>Nutzerfuehrung</td><td>Verkehrsart zu Beginn.</td><td>NIEDRIG</td><td>S</td><td>&#8212;</td><td>Offen</td></tr>
</table>
<h3>Team Zero (16 Themen)</h3>
<table>
<tr><th>#</th><th>Thema</th><th>Kurzfassung</th><th>Prio</th><th>Dauer</th><th>Deadline</th><th>Status</th></tr>
<tr style="background:#f8cecc;"><td>Z1</td><td><strong>Spring Boot 4 (CI, AV)</strong></td><td>AV-Trasse Test-Branch aktiv.</td><td>MUSS</td><td>M</td><td><strong>Jun 2026</strong></td><td>&#128260;</td></tr>
<tr style="background:#fff2cc;"><td>Z2</td><td>NEP2 Schnittstellen</td><td>Aktueller TTT-Meilenstein.</td><td>HOCH</td><td>Q</td><td>Laufend</td><td>&#128260;</td></tr>
<tr style="background:#fff2cc;"><td>Z3</td><td>Workarounds TTK aufraeumen</td><td>Technische Schuld.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
<tr style="background:#fff2cc;"><td>Z4</td><td>Performance AV</td><td>Auftrags-Verwaltung.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
<tr style="background:#fff2cc;"><td>Z5</td><td>Verschluesselung</td><td>Security-Anforderung.</td><td>HOCH</td><td>S</td><td>PI 41</td><td>Offen</td></tr>
<tr><td>Z6</td><td>Monitoring Grafana</td><td>Boards nutzbar machen.</td><td>MITTEL</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
<tr><td>Z7</td><td>Stationsportal</td><td>Fertigstellen.</td><td>MITTEL</td><td>M</td><td>PI 41/42</td><td>Offen</td></tr>
<tr><td>Z8</td><td>LuP VNP Versand</td><td>Last- und Performance.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
<tr><td>Z9</td><td>PCS aufraeumen</td><td>Gestaltung Rueckweg. Gross.</td><td>MITTEL</td><td>Q</td><td>PI 42/43</td><td>Offen</td></tr>
<tr><td>Z10</td><td>Runbook</td><td>Aktualisieren.</td><td>NIEDRIG</td><td>M</td><td>Laufend</td><td>Offen</td></tr>
<tr><td>Z11</td><td>Click&amp;Ride</td><td>Mit CIB.</td><td>NIEDRIG</td><td>Q</td><td>PI 43+</td><td>Offen</td></tr>
<tr><td>Z12</td><td>Infrastruktur Revisionen</td><td>Verarbeiten.</td><td>NIEDRIG</td><td>M</td><td>&#8212;</td><td>Offen</td></tr>
<tr><td>Z13</td><td>Ersatz Message Routing ID</td><td>SenderReference. Gross.</td><td>NIEDRIG</td><td>Q</td><td>&#8212;</td><td>Offen</td></tr>
<tr><td>Z14</td><td>XSD 3.5.2</td><td>Kann gross werden.</td><td>NIEDRIG</td><td>Q</td><td>&#8212;</td><td>Offen</td></tr>
<tr><td>Z15</td><td>LuP neu</td><td>Komplett neu.</td><td>NIEDRIG</td><td>L</td><td>&#8212;</td><td>Offen</td></tr>
<tr><td>Z16</td><td>TDM aufraeumen</td><td>Datenmodell.</td><td>NIEDRIG</td><td>M</td><td>&#8212;</td><td>Offen</td></tr>
</table>
<h3>OPs / DevOps (14 Themen)</h3>
<table>
<tr><th>#</th><th>Thema</th><th>Kurzfassung</th><th>Prio</th><th>Dauer</th><th>Deadline</th><th>Status</th></tr>
<tr style="background:#f8cecc;"><td>O1</td><td><strong>Netcool-Anbindung</strong></td><td>Betriebsueberwachung. UKA.</td><td>MUSS</td><td>M</td><td>PI 40/41</td><td>&#128260;</td></tr>
<tr style="background:#f8cecc;"><td>O2</td><td><strong>Monitoring-Konzept</strong></td><td>Umsetzung seit PI 40.</td><td>MUSS</td><td>M</td><td>PI 41</td><td>&#128260;</td></tr>
<tr style="background:#f8cecc;"><td>O3</td><td><strong>SL Silber Plus</strong></td><td>Vertragliche Verpflichtung.</td><td>MUSS</td><td>Q</td><td><strong>Aug 2026</strong></td><td>Offen</td></tr>
<tr style="background:#fff2cc;"><td>O4</td><td>DR-Test 3</td><td>SL-Voraussetzung.</td><td>HOCH</td><td>M</td><td>PI 40/41</td><td>&#128260;</td></tr>
<tr style="background:#fff2cc;"><td>O5</td><td>Secret-Rotation</td><td>Security-Konzept.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
<tr style="background:#fff2cc;"><td>O6</td><td>Deployment-Automatisierung</td><td>Smoketests. Seit 03/2026.</td><td>HOCH</td><td>Q</td><td>PI 41/42</td><td>&#128260;</td></tr>
<tr style="background:#fff2cc;"><td>O7</td><td>K8s-Cluster/Namespaces</td><td>Umstellung. UKA.</td><td>HOCH</td><td>M</td><td>PI 40</td><td>&#128260;</td></tr>
<tr style="background:#fff2cc;"><td>O8</td><td>Jan Lubenow entlasten</td><td>SPOF 39%. Wissenstransfer.</td><td>HOCH</td><td>L</td><td>Laufend</td><td>&#9888;</td></tr>
<tr><td>O9</td><td>AppMesh-Alternative</td><td>AWS EOL 2026.</td><td>MITTEL</td><td>Q</td><td>PI 42</td><td>Offen</td></tr>
<tr><td>O10</td><td>Umgebungen konsolidieren</td><td>17+ reduzieren.</td><td>MITTEL</td><td>Q</td><td>PI 42/43</td><td>Offen</td></tr>
<tr><td>O11</td><td>Keycloak konsolidieren</td><td>5+ Repos.</td><td>NIEDRIG</td><td>M</td><td>&#8212;</td><td>Offen</td></tr>
<tr><td>O12</td><td>ECR statt Artifactory</td><td>Laufzeit-Artefakte. UKA.</td><td>MITTEL</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
<tr><td>O13</td><td>Security-Groups egress</td><td>Netzwerk-Security.</td><td>MITTEL</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
<tr><td>O14</td><td>Runbook beschleunigen</td><td>DevOps-Doku.</td><td>MITTEL</td><td>L</td><td>Laufend</td><td>&#128260;</td></tr>
</table>
<h3>Uebergreifend / Programm (13 Themen)</h3>
<table>
<tr><th>#</th><th>Thema</th><th>Kurzfassung</th><th>Prio</th><th>Dauer</th><th>Deadline</th><th>Status</th></tr>
<tr style="background:#fff2cc;"><td>U1</td><td><strong>Hypercare beenden</strong></td><td>Normalbetrieb.</td><td>HOCH</td><td>&#8212;</td><td>Jul 2026</td><td>Offen</td></tr>
<tr style="background:#fff2cc;"><td>U2</td><td><strong>GelV Erweiterungen</strong></td><td>TTT-Meilenstein.</td><td>HOCH</td><td>Q</td><td>Sep 2026</td><td>Offen</td></tr>
<tr style="background:#fff2cc;"><td>U3</td><td>ujBau Funktionalitaet</td><td>TTT-Meilenstein.</td><td>HOCH</td><td>Q</td><td>Laufend</td><td>&#128260;</td></tr>
<tr style="background:#fff2cc;"><td>U4</td><td>FplJ 28 Vorbereitung</td><td>Fahrplanwechsel Dez 2026.</td><td>HOCH</td><td>Q</td><td><strong>Dez 2026</strong></td><td>Offen</td></tr>
<tr style="background:#fff2cc;"><td>U5</td><td>DB Cargo Eskalation CIO</td><td>TTTSOL-2186. PCS + VNP.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>&#128260;</td></tr>
<tr style="background:#fff2cc;"><td>U6</td><td>OTN-Vergabe Fpl 2027</td><td>Muss vor FplW geloest sein.</td><td>HOCH</td><td>M</td><td>Dez 2026</td><td>&#128260;</td></tr>
<tr style="background:#fff2cc;"><td>U7</td><td>Ad-hoc Verkehre &lt;1h</td><td>BLOCKED. TopThema. FV.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>&#10060;</td></tr>
<tr style="background:#fff2cc;"><td>U8</td><td>Mittiger Teilausfall SEV</td><td>Fernverkehr. Taskforce.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>&#128260;</td></tr>
<tr><td>U9</td><td>Team-Reorganisation</td><td>Subdomaenen-Schnitt.</td><td>MITTEL</td><td>Q</td><td>Q1 2027</td><td>Offen</td></tr>
<tr><td>U10</td><td>Support-Enablement</td><td>Tools, Zugang, Prozesse.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
<tr><td>U11</td><td>Contract Testing</td><td>Cross-Team Qualitaet.</td><td>NIEDRIG</td><td>Q</td><td>PI 43</td><td>Offen</td></tr>
<tr><td>U12</td><td>TPN abloesen</td><td>Parallelbetrieb beenden.</td><td>NIEDRIG</td><td>L</td><td>Q2 2027</td><td>Offen</td></tr>
<tr><td>U13</td><td>Daten-Partitionierung</td><td>Performance wachsende Daten.</td><td>MITTEL</td><td>Q</td><td>Dez 2026</td><td>Offen</td></tr>
</table>
<h3>Zusammenfassung</h3>
<table>
<tr><th>Team</th><th>MUSS</th><th>HOCH</th><th>MITTEL</th><th>NIEDRIG</th><th>Gesamt</th></tr>
<tr><td><strong>CIB</strong></td><td>4</td><td>5</td><td>5</td><td>3</td><td>17</td></tr>
<tr><td><strong>404</strong></td><td>0</td><td>7</td><td>7</td><td>3</td><td>17</td></tr>
<tr><td><strong>Zero</strong></td><td>1</td><td>4</td><td>4</td><td>7</td><td>16</td></tr>
<tr><td><strong>OPs/DevOps</strong></td><td>3</td><td>5</td><td>4</td><td>2</td><td>14</td></tr>
<tr><td><strong>Uebergreifend</strong></td><td>0</td><td>8</td><td>3</td><td>2</td><td>13</td></tr>
<tr style="background:#f5f5f5;"><td><strong>GESAMT</strong></td><td><strong>8</strong></td><td><strong>29</strong></td><td><strong>23</strong></td><td><strong>17</strong></td><td><strong>77</strong></td></tr>
</table>
"@
# Create or Update
$SearchUri = "$ApiBase/content?spaceKey=$SpaceKey&title=$([Uri]::EscapeDataString($Title))&type=page"
try {
$SearchResp = Invoke-WebRequest -Uri $SearchUri -Headers $Headers -UseBasicParsing -ErrorAction Stop
$SearchData = $SearchResp.Content | ConvertFrom-Json
} catch { Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red; exit 1 }
if ($SearchData.results -and $SearchData.results.Count -gt 0) {
$PageId = $SearchData.results[0].id
$VerResp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId`?expand=version" -Headers $Headers -UseBasicParsing
$VerData = $VerResp.Content | ConvertFrom-Json
$Version = $VerData.version.number + 1
Write-Host " Aktualisiere (ID: $PageId, v$Version)..." -NoNewline
$Body = @{ version = @{ number = $Version }; title = $Title; type = "page"; body = @{ storage = @{ value = $Html; representation = "storage" } } } | ConvertTo-Json -Depth 10
$Resp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId" -Method Put -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($Body)) -UseBasicParsing
Write-Host " OK" -ForegroundColor Green
$ResultId = ($Resp.Content | ConvertFrom-Json).id
} else {
Write-Host " Erstelle neue Seite..." -NoNewline
$Body = @{ type = "page"; title = $Title; space = @{ key = $SpaceKey }; ancestors = @(@{ id = $ParentPageId }); body = @{ storage = @{ value = $Html; representation = "storage" } } } | ConvertTo-Json -Depth 10
$Resp = Invoke-WebRequest -Uri "$ApiBase/content" -Method Post -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($Body)) -UseBasicParsing
Write-Host " OK" -ForegroundColor Green
$ResultId = ($Resp.Content | ConvertFrom-Json).id
}
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " Seite: $ConfluenceUrl/pages/viewpage.action?pageId=$ResultId" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
@@ -0,0 +1,161 @@
"""Pusht Team-Reorganisation als Confluence-Seite."""
import urllib.request, json, ssl, urllib.parse
from pathlib import Path
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
TOKEN = secrets.get("CONFLUENCE_TOKEN", "")
BASE = "https://arija-confluence.jaas.service.deutschebahn.com/rest/api"
SPACE = "BES"
PARENT = "581013135"
TITLE = "25. Team-Reorganisation (Optionen)"
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
# Read the markdown and convert key sections to HTML
html = """<h2>Team-Reorganisation pathOS &#8212; Optionen und Bewertung</h2>
<p><strong>Stand:</strong> 2026-04-30 | Status: Entwurf zur Diskussion</p>
<ac:structured-macro ac:name="info"><ac:rich-text-body>
<p>Dieses Dokument analysiert Optionen fuer die Neuaufstellung der pathOS-Teams. Ziele: Schnellere Umsetzung, weniger Komplexitaet, bessere Qualitaet und Verantwortlichkeit.</p>
</ac:rich-text-body></ac:structured-macro>
<h3>Ausgangslage</h3>
<table>
<tr><th>Team</th><th>Personen</th><th>Verantwortung</th><th>Services</th><th>Problem</th></tr>
<tr><td>Team 404</td><td>~10</td><td>Portal UI + Middleware</td><td>2 (68K LoC)</td><td>33% Bug-Rate, steigend</td></tr>
<tr><td>Team CIB</td><td>~13</td><td>Prozesse, Backend, Camunda</td><td>~15 (122K LoC)</td><td>SV-Monolith (93K, 2214 Smells)</td></tr>
<tr><td>Team Zero</td><td>~9</td><td>TAF/TAP Schnittstellen</td><td>~9 (38K LoC)</td><td>OPs-Rotation belastet</td></tr>
<tr><td>OPs Squad</td><td>2 fest + rot.</td><td>Infrastruktur, Betrieb</td><td>Infra-Repos</td><td>Nur 2 feste Mitglieder</td></tr>
<tr><td>DevOps</td><td>~7</td><td>CI/CD, Tooling</td><td>Pipelines</td><td>Jan Lubenow = 39% SPOF</td></tr>
</table>
<h3>Ziele</h3>
<table>
<tr><th>Primaer</th><th>Nachrangig</th></tr>
<tr><td>Schnelle Umsetzungsgeschwindigkeit</td><td>Staerkenorientierter Einsatz</td></tr>
<tr><td>Weniger Komplexitaet</td><td>Wissen breiter verteilen</td></tr>
<tr><td>Weniger Abhaengigkeiten</td><td>Flexibilitaet bei Prioritaetswechsel</td></tr>
<tr><td>Bessere Qualitaet</td><td></td></tr>
<tr><td>Bessere Verantwortlichkeit</td><td></td></tr>
</table>
<h3>Option A: OpsDev + Feature-Pool</h3>
<ac:structured-macro ac:name="panel"><ac:parameter ac:name="title">Modell</ac:parameter><ac:rich-text-body>
<p><strong>OpsDev Team</strong> (permanent, ~8-10): Betrieb, Bugs, kleine Features. Lead OpsDev ohne PO.</p>
<p><strong>Feature-Pool</strong> (~20-25): Entwickler + BAs + Feature-POs. Bilden temporaere Feature-Teams (3-5 Pers.) fuer grosse Features. Nach Abschluss: 1 Dev &#8594; OpsDev (Hypercare).</p>
</ac:rich-text-body></ac:structured-macro>
<table>
<tr><th>Vorteile</th><th>Nachteile</th></tr>
<tr><td>Feature-Team ownt end-to-end</td><td>Kontextwechsel, Onboarding-Aufwand</td></tr>
<tr><td>Keine Cross-Team-Deps fuer Features</td><td>Kein stabiles Team, Teambildung leidet</td></tr>
<tr><td>Flexibel nach Prioritaet</td><td>OpsDev wird Muellhalde fuer Bugs</td></tr>
<tr><td>Wissenstransfer durch Hypercare</td><td>PO-Overhead (3-4 POs parallel)</td></tr>
</table>
<h3>Option B: Fachlicher Schnitt (4 Varianten)</h3>
<h4>B1: Bestellen vs. Abwickeln</h4>
<table>
<tr><th>Team "Bestellen"</th><th>Team "Abwickeln"</th></tr>
<tr><td>Portal UI + MW, Common Interface, Stammdaten, Kundendaten, TAF/TAP-Konverter</td><td>Steuerung Vertrieb (Camunda), Auftrags-Verwaltung, IFP-Connector, Vertragsdaten, Archivierung, Abrechnung</td></tr>
<tr><td><em>Fokus: Was der Kunde sieht</em></td><td><em>Fokus: Was nach der Bestellung passiert</em></td></tr>
</table>
<p>&#10004; Klarer Kundenfokus | &#10008; NAÄ betrifft beide, SV ist Monolith</p>
<h4>B4: Trasse vs. Vertrag (bester fachlicher Schnitt)</h4>
<table>
<tr><th>Team "Trasse"</th><th>Team "Vertrag"</th></tr>
<tr><td>Trassenanmeldung (Portal+CI), Trassenkonstruktion (IFP), Stammdaten, TAF/TAP</td><td>Angebot + Vertrag (SV), Abrechnung, Stornierung, Rahmenvertraege, Vertragsdaten</td></tr>
<tr><td><em>Vom Kundenwunsch bis Konstruktionsauftrag</em></td><td><em>Vom Angebot bis zur Rechnung</em></td></tr>
</table>
<p>&#10004; Sauberster Schnitt entlang Geschaeftsprozess, Abrechnung hat eigenes Team | &#10008; SV muesste aufgeteilt werden, Portal zeigt beides</p>
<h3>Option C: Hybrid &#8212; EMPFOHLEN</h3>
<ac:structured-macro ac:name="tip"><ac:rich-text-body>
<p><strong>Empfohlenes Modell:</strong> OpsDev (permanent) + 2 fachliche Teams (permanent) + temporaere Feature-Squads fuer grosse Themen.</p>
</ac:rich-text-body></ac:structured-macro>
<table>
<tr><th>OpsDev (~6 Pers.)</th><th>Team "Bestellen" (~12)</th><th>Team "Verarbeiten" (~12)</th></tr>
<tr><td>Betrieb, Deployment<br/>Monitoring, Infrastruktur<br/>Bug-Triage<br/>Lead: OpsDev-Lead</td><td>Portal UI + MW<br/>Common Interface<br/>Stammdaten, Kundendaten<br/>TAF/TAP Konverter<br/>Click&amp;Ride<br/>PO + BA + Devs</td><td>Steuerung Vertrieb<br/>Auftrags-Verwaltung<br/>IFP-Connector<br/>Archivierung<br/>Vertragsdaten, Abrechnung<br/>PO + BA + Devs</td></tr>
<tr><td>Bugs: Infrastruktur</td><td>Bugs: Portal, CI, STB</td><td>Bugs: SV, AV, IFP</td></tr>
</table>
<p><strong>Fuer grosse Features</strong> (GelV, ujBau, NAÄ): Temporaer 2-3 Personen aus beiden Teams zusammenziehen. Nach Abschluss: zurueck + 1 Person Hypercare in OpsDev.</p>
<h3>Gesamtbewertung</h3>
<table>
<tr><th>Option</th><th>Speed</th><th>Komplexitaet</th><th>Deps</th><th>Qualitaet</th><th>Ownership</th></tr>
<tr><td>A: OpsDev + Pool</td><td>&#11088;&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;</td></tr>
<tr><td>B1: Bestellen/Abwickeln</td><td>&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;&#11088;</td></tr>
<tr><td>B4: Trasse/Vertrag</td><td>&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;&#11088;</td></tr>
<tr style="background:#d5e8d4;"><td><strong>C: Hybrid (empfohlen)</strong></td><td>&#11088;&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;&#11088;&#11088;</td></tr>
<tr><td>D: Spotify</td><td>&#11088;&#11088;&#11088;</td><td>&#11088;</td><td>&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;</td><td>&#11088;&#11088;&#11088;</td></tr>
</table>
<h3>Noch zu klaeren</h3>
<ol>
<li><strong>SV aufteilen?</strong> &#8212; Ist der Monolith (93K LoC) technisch teilbar?</li>
<li><strong>NAÄ als Querschnitt</strong> &#8212; Dediziertes Feature-Team oder feste Zuordnung?</li>
<li><strong>Portal-Ownership</strong> &#8212; Portal zeigt Daten aus allen Services. Eigener Querschnitt?</li>
<li><strong>Abrechnung</strong> &#8212; Eigenes Team wert? Oder bei "Verarbeiten"?</li>
<li><strong>Personelle Passung</strong> &#8212; Staerken-Mapping der 35 Personen</li>
<li><strong>Uebergangsphase</strong> &#8212; Dauer, Produktivitaetsverlust?</li>
<li><strong>PO-Struktur</strong> &#8212; 1 PO pro Team oder uebergreifend + Feature-POs?</li>
<li><strong>Metriken</strong> &#8212; Bug-Rate, Durchlaufzeit, Deployment-Frequenz als Erfolgsmessung</li>
</ol>
<h3>Fehlende Daten fuer Entscheidung</h3>
<ul>
<li>Git-Contributions pro Person/Service (wer kennt welchen Code?)</li>
<li>Abhaengigkeits-Graph zwischen Services (Kafka-Topics, REST-Calls)</li>
<li>Kundenfeedback: Welche Features werden am meisten nachgefragt?</li>
<li>Camunda-Prozess-Instanzen: Wie oft laeuft welcher Prozess?</li>
</ul>
"""
# Push to Confluence
def api_request(url, method="GET", data=None):
req = urllib.request.Request(url, method=method)
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("Content-Type", "application/json")
if data:
req.data = json.dumps(data).encode("utf-8")
return json.loads(urllib.request.urlopen(req, context=ctx, timeout=30).read().decode("utf-8"))
# Check if page exists
search_url = f"{BASE}/content?spaceKey={SPACE}&title={urllib.parse.quote(TITLE)}&type=page"
req = urllib.request.Request(search_url)
req.add_header("Authorization", f"Bearer {TOKEN}")
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
search_data = json.loads(resp.read().decode("utf-8"))
if search_data.get("results"):
# Update
page_id = search_data["results"][0]["id"]
ver_url = f"{BASE}/content/{page_id}?expand=version"
req2 = urllib.request.Request(ver_url)
req2.add_header("Authorization", f"Bearer {TOKEN}")
ver_data = json.loads(urllib.request.urlopen(req2, context=ctx, timeout=30).read().decode("utf-8"))
version = ver_data["version"]["number"] + 1
body = json.dumps({"version": {"number": version}, "title": TITLE, "type": "page", "body": {"storage": {"value": html, "representation": "storage"}}}).encode("utf-8")
req3 = urllib.request.Request(f"{BASE}/content/{page_id}", method="PUT", data=body)
req3.add_header("Authorization", f"Bearer {TOKEN}")
req3.add_header("Content-Type", "application/json")
result = json.loads(urllib.request.urlopen(req3, context=ctx, timeout=30).read().decode("utf-8"))
print(f"Aktualisiert: https://arija-confluence.jaas.service.deutschebahn.com/pages/viewpage.action?pageId={result['id']}")
else:
# Create
body = json.dumps({"type": "page", "title": TITLE, "space": {"key": SPACE}, "ancestors": [{"id": PARENT}], "body": {"storage": {"value": html, "representation": "storage"}}}).encode("utf-8")
req3 = urllib.request.Request(f"{BASE}/content", method="POST", data=body)
req3.add_header("Authorization", f"Bearer {TOKEN}")
req3.add_header("Content-Type", "application/json")
result = json.loads(urllib.request.urlopen(req3, context=ctx, timeout=30).read().decode("utf-8"))
print(f"Erstellt: https://arija-confluence.jaas.service.deutschebahn.com/pages/viewpage.action?pageId={result['id']}")
@@ -0,0 +1,128 @@
"""Pusht Team-Uebersicht inkl. TrassenOrder als Confluence-Seite."""
import urllib.request, json, ssl, urllib.parse
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
TOKEN = secrets.get("CONFLUENCE_TOKEN", "")
BASE = "https://arija-confluence.jaas.service.deutschebahn.com/rest/api"
SPACE = "BES"
PARENT = "581013135"
TITLE = "26. Teams inkl. TrassenOrder"
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
html = """<h2>Team-Landschaft pathOS + TrassenOrder</h2>
<p><strong>Stand:</strong> 2026-04-30 | Kontext: Reorganisations-Planung</p>
<ac:structured-macro ac:name="info"><ac:rich-text-body>
<p><strong>TrassenOrder (TraPo)</strong> ist ein Testballon, um zu pruefen ob eine andere Herangehensweise (entkoppelt, UX-first, klein) schneller, besser und sicherer funktioniert als der pathOS-Ansatz (Microservice-Monolith, 167 Repos, 35+ Personen).</p>
</ac:rich-text-body></ac:structured-macro>
<h3>Aktuelle Team-Landschaft</h3>
<table>
<tr><th>Team</th><th>Personen</th><th>Fokus</th><th>Services</th><th>Deployment</th><th>Besonderheit</th></tr>
<tr><td><strong>Team 404</strong></td><td>~10</td><td>Portal UI + Middleware</td><td>2 (68K LoC)</td><td>pathOS Release-Zug</td><td>33% Bug-Rate, kundenseitig</td></tr>
<tr><td><strong>Team CIB</strong></td><td>~13</td><td>Prozesse, Camunda, Backend</td><td>~15 (122K LoC)</td><td>pathOS Release-Zug</td><td>SV-Monolith, Abrechnung</td></tr>
<tr><td><strong>Team Zero</strong></td><td>~9</td><td>TAF/TAP Schnittstellen</td><td>~9 (38K LoC)</td><td>pathOS Release-Zug</td><td>Beste Qualitaet, OPs-Rotation</td></tr>
<tr><td><strong>OPs Squad</strong></td><td>2 fest + rot.</td><td>Betrieb, Infrastruktur</td><td>Infra-Repos</td><td>pathOS Release-Zug</td><td>Seit PI 39 (ex-STeam)</td></tr>
<tr><td><strong>DevOps</strong></td><td>~7</td><td>CI/CD, Tooling, Pipelines</td><td>Pipelines, Helm</td><td>pathOS Release-Zug</td><td>Jan Lubenow = 39% SPOF</td></tr>
<tr style="background:#dae8fc;"><td><strong>TrassenOrder (TraPo)</strong></td><td>~5 (klein)</td><td>GelV-Portal (UX-first)</td><td>1 (neu)</td><td><strong>Unabhaengig!</strong></td><td>Testballon, Discovery-Phase</td></tr>
</table>
<h3>TrassenOrder vs. pathOS &#8212; Vergleich der Ansaetze</h3>
<table>
<tr><th>Dimension</th><th>pathOS (aktuell)</th><th>TrassenOrder (Testballon)</th></tr>
<tr><td><strong>Architektur</strong></td><td>Microservice-Monolith (synchrone Releases, 167 Repos)</td><td>Externes System, eigene Schnittstellen, unabhaengig deploybar</td></tr>
<tr><td><strong>Teamgroesse</strong></td><td>~35+ Personen, 5 Teams</td><td>~5 Personen, 1 Team</td></tr>
<tr><td><strong>Zielgruppe</strong></td><td>Alle EVUs (Experten + Anfaenger)</td><td>Kleine EVUs, Ein-Mann-Betriebe ("WhatsApp-Nutzer")</td></tr>
<tr><td><strong>UX-Ansatz</strong></td><td>240+ Formularfelder, Experten-Tool</td><td>3 Angaben fuer eine Bestellung, radikal vereinfacht</td></tr>
<tr><td><strong>Deployment</strong></td><td>Release-Zug (alle Services zusammen)</td><td>Unabhaengig, eigener Rhythmus</td></tr>
<tr><td><strong>Abhaengigkeiten</strong></td><td>Hoch (Kafka, Camunda, 15+ Services)</td><td>Minimal (nur API-Schnittstellen zu Primaerquellen)</td></tr>
<tr><td><strong>Geschwindigkeit</strong></td><td>PI-getaktet (10 Wochen)</td><td>Kontinuierlich, Feature-basiert</td></tr>
<tr><td><strong>Scope</strong></td><td>Netzfahrplan + GelV + ujBau + Abrechnung</td><td>Nur GelV (Gelegenheitsverkehr)</td></tr>
<tr><td><strong>Phase</strong></td><td>Produktiv seit Dez 2025</td><td>Discovery seit Jan 2026, Livegang Q4 2026</td></tr>
</table>
<h3>Was testet der Testballon?</h3>
<ac:structured-macro ac:name="panel"><ac:parameter ac:name="title">Hypothesen</ac:parameter><ac:rich-text-body>
<ol>
<li><strong>Schneller:</strong> Kann ein kleines, entkoppeltes Team schneller liefern als ein grosses im Release-Zug?</li>
<li><strong>Besser:</strong> Fuehrt UX-first + radikale Vereinfachung zu besserer Nutzerzufriedenheit?</li>
<li><strong>Sicherer:</strong> Reduziert Entkopplung das Risiko (kein Dominoeffekt bei Fehlern)?</li>
</ol>
<p><strong>Wenn der Testballon erfolgreich ist:</strong> Das Modell koennte auf weitere Bereiche uebertragen werden (Netzfahrplan, ujBau). Langfristig koennte TrassenOrder das pathOS-Portal <strong>abloesen</strong>.</p>
</ac:rich-text-body></ac:structured-macro>
<h3>Schnittstellen zwischen pathOS und TrassenOrder</h3>
<table>
<tr><th>Schnittstelle</th><th>Richtung</th><th>Zweck</th></tr>
<tr><td>Trassenanmeldung API</td><td>TraPo &#8594; pathOS</td><td>Bestellung absetzen</td></tr>
<tr><td>Stammdaten API</td><td>TraPo &#8592; Primaerquelle</td><td>Direkt, NICHT ueber pathOS</td></tr>
<tr><td>Angebots-Rueckmeldung</td><td>pathOS &#8594; TraPo</td><td>Ergebnis der Konstruktion</td></tr>
<tr><td>Trassenfinder</td><td>TraPo &#8592; BVU</td><td>Routing, Validierung (Fernziel)</td></tr>
</table>
<p><strong>Architekturentscheidung:</strong> TrassenOrder greift auf Primaerquellen direkt zu &#8212; nicht ueber eine pathOS-Zwischenschicht. Das vermeidet Abhaengigkeiten vom pathOS Release-Zug.</p>
<h3>Implikationen fuer Team-Reorganisation</h3>
<table>
<tr><th>Szenario</th><th>Auswirkung auf pathOS-Teams</th></tr>
<tr><td><strong>TraPo erfolgreich &#8594; GelV wandert zu TraPo</strong></td><td>pathOS kann sich auf Netzfahrplan + ujBau + Abrechnung konzentrieren. Vereinfacht den fachlichen Schnitt erheblich. Click&amp;Ride (ADR-72) wird obsolet.</td></tr>
<tr><td><strong>TraPo erfolgreich &#8594; Modell wird uebertragen</strong></td><td>Weitere kleine Teams fuer spezifische Domaenen. pathOS wird zum API-Backend-Layer. Portal-Team (404) wird langfristig obsolet.</td></tr>
<tr><td><strong>TraPo scheitert &#8594; GelV bleibt bei pathOS</strong></td><td>Keine Aenderung. Click&amp;Ride Anbindung (ADR-72) wird weiter von CIB gebaut.</td></tr>
</table>
<h3>Offene Fragen</h3>
<ul>
<li>Wann ist der Testballon "erfolgreich"? Welche Metriken? (Time-to-Market, Nutzerzufriedenheit, Bug-Rate?)</li>
<li>Wie wird die Schnittstelle zwischen TraPo und pathOS-Backend definiert und versioniert?</li>
<li>Wer pflegt die Schnittstelle langfristig? (API-Vertrag)</li>
<li>Kann das TraPo-Modell auf Netzfahrplan skaliert werden? (Komplexitaet ist dort 10x hoeher)</li>
<li>Was passiert mit Team 404 wenn TrassenOrder das Portal langfristig abloest?</li>
</ul>
<h3>Team-Kennzahlen (Vergleich)</h3>
<table>
<tr><th>Metrik</th><th>pathOS gesamt</th><th>TrassenOrder</th><th>Faktor</th></tr>
<tr><td>Personen</td><td>~35</td><td>~5</td><td>7x</td></tr>
<tr><td>Services</td><td>~40 aktiv</td><td>1</td><td>40x</td></tr>
<tr><td>Lines of Code</td><td>~260K</td><td>~0 (Discovery)</td><td>&#8212;</td></tr>
<tr><td>Jira-Projekte</td><td>6 (O2C*, TTTI)</td><td>1 (TRAPO)</td><td>6x</td></tr>
<tr><td>Release-Frequenz</td><td>~2x/Monat (KTU)</td><td>Kontinuierlich (Ziel)</td><td>&#8212;</td></tr>
<tr><td>Bug-Rate</td><td>20-33%</td><td>0% (noch kein Code)</td><td>&#8212;</td></tr>
<tr><td>Deployment-Abhaengigkeiten</td><td>Hoch (17+ Umgebungen)</td><td>Keine</td><td>&#8212;</td></tr>
</table>
"""
# Push
search_url = f"{BASE}/content?spaceKey={SPACE}&title={urllib.parse.quote(TITLE)}&type=page"
req = urllib.request.Request(search_url)
req.add_header("Authorization", f"Bearer {TOKEN}")
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
search_data = json.loads(resp.read().decode("utf-8"))
if search_data.get("results"):
page_id = search_data["results"][0]["id"]
ver_url = f"{BASE}/content/{page_id}?expand=version"
req2 = urllib.request.Request(ver_url)
req2.add_header("Authorization", f"Bearer {TOKEN}")
ver_data = json.loads(urllib.request.urlopen(req2, context=ctx, timeout=30).read().decode("utf-8"))
version = ver_data["version"]["number"] + 1
body = json.dumps({"version": {"number": version}, "title": TITLE, "type": "page", "body": {"storage": {"value": html, "representation": "storage"}}}).encode("utf-8")
req3 = urllib.request.Request(f"{BASE}/content/{page_id}", method="PUT", data=body)
req3.add_header("Authorization", f"Bearer {TOKEN}")
req3.add_header("Content-Type", "application/json")
result = json.loads(urllib.request.urlopen(req3, context=ctx, timeout=30).read().decode("utf-8"))
print(f"Aktualisiert: https://arija-confluence.jaas.service.deutschebahn.com/pages/viewpage.action?pageId={result['id']}")
else:
body = json.dumps({"type": "page", "title": TITLE, "space": {"key": SPACE}, "ancestors": [{"id": PARENT}], "body": {"storage": {"value": html, "representation": "storage"}}}).encode("utf-8")
req3 = urllib.request.Request(f"{BASE}/content", method="POST", data=body)
req3.add_header("Authorization", f"Bearer {TOKEN}")
req3.add_header("Content-Type", "application/json")
result = json.loads(urllib.request.urlopen(req3, context=ctx, timeout=30).read().decode("utf-8"))
print(f"Erstellt: https://arija-confluence.jaas.service.deutschebahn.com/pages/viewpage.action?pageId={result['id']}")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,105 @@
<#
.SYNOPSIS
Liest spezifische Confluence-Seiten per API
#>
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
$ApiBase = "$ConfluenceUrl/rest/api"
$OutputDir = "project-audit\data\confluence-ttsi"
$PageIds = @(
@{ id = "424295009"; name = "TAF-TAP-TSI-Programmakte" }
@{ id = "428930895"; name = "TAF-TAP-TSI-Programmsteuerung" }
@{ id = "490596078"; name = "TAF-TAP-TSI-Fachliche-Dokumentation" }
)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "============================================================"
Write-Host " Confluence TTSI-Space Seiten lesen"
Write-Host "============================================================"
$PAT = Read-Host "Confluence PAT"
$Headers = @{ "Authorization" = "Bearer $PAT" }
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue
function Convert-HtmlToText($html) {
if ($null -eq $html) { return "" }
$text = $html -replace '<br\s*/?>', "`n"
$text = $text -replace '<p[^>]*>', "`n"
$text = $text -replace '</p>', ""
$text = $text -replace '<li[^>]*>', "`n- "
$text = $text -replace '<h[1-6][^>]*>', "`n## "
$text = $text -replace '</h[1-6]>', "`n"
$text = $text -replace '<tr[^>]*>', "`n| "
$text = $text -replace '<td[^>]*>', " | "
$text = $text -replace '<th[^>]*>', " | "
$text = $text -replace '<[^>]+>', ''
try { $text = [System.Web.HttpUtility]::HtmlDecode($text) } catch {}
return $text.Trim()
}
foreach ($Page in $PageIds) {
$PageId = $Page.id
$PName = $Page.name
Write-Host ""
Write-Host ">> $PName (ID: $PageId)" -ForegroundColor Yellow
$Uri = "$ApiBase/content/$PageId`?expand=body.storage,version,space,children.page"
try {
$Resp = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
$Data = $Resp.Content | ConvertFrom-Json
Write-Host " Titel: $($Data.title) (v$($Data.version.number))" -ForegroundColor Green
# HTML speichern
$HtmlContent = ""
if ($Data.body -and $Data.body.storage) { $HtmlContent = $Data.body.storage.value }
$HtmlContent | Out-File "$OutputDir\$PName.html" -Encoding UTF8
# Markdown speichern
$TextContent = Convert-HtmlToText $HtmlContent
$MdContent = "# $($Data.title)`n`n> Page ID: $PageId | Version: $($Data.version.number) | Space: $($Data.space.key)`n`n---`n`n$TextContent"
$MdContent | Out-File "$OutputDir\$PName.md" -Encoding UTF8
Write-Host " Gespeichert ($($HtmlContent.Length) Zeichen)"
# Kindseiten auflisten
if ($Data.children -and $Data.children.page -and $Data.children.page.results) {
Write-Host " Kindseiten:" -ForegroundColor DarkGray
foreach ($Child in $Data.children.page.results) {
Write-Host " - $($Child.title) (ID: $($Child.id))"
}
# Kindseiten auch lesen
foreach ($Child in $Data.children.page.results) {
Write-Host " >> Kindseite: $($Child.title)..." -NoNewline
$ChildUri = "$ApiBase/content/$($Child.id)`?expand=body.storage,version"
try {
$CResp = Invoke-WebRequest -Uri $ChildUri -Headers $Headers -UseBasicParsing -ErrorAction Stop
$CData = $CResp.Content | ConvertFrom-Json
$CHtml = ""
if ($CData.body -and $CData.body.storage) { $CHtml = $CData.body.storage.value }
$CText = Convert-HtmlToText $CHtml
$SafeTitle = $Child.title -replace '[\\/:*?"<>|]', '_'
if ($SafeTitle.Length -gt 60) { $SafeTitle = $SafeTitle.Substring(0, 60) }
$CMd = "# $($Child.title)`n`n> Page ID: $($Child.id) | Parent: $PName`n`n---`n`n$CText"
$CMd | Out-File "$OutputDir\$PName`_$SafeTitle.md" -Encoding UTF8
$CHtml | Out-File "$OutputDir\$PName`_$SafeTitle.html" -Encoding UTF8
Write-Host " OK" -ForegroundColor Green
} catch {
Write-Host " FEHLER" -ForegroundColor Red
}
}
}
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
}
}
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " FERTIG! Ergebnisse in $OutputDir" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
@@ -0,0 +1,283 @@
<#
.SYNOPSIS
Reorganisiert die Analyse-pathOS Confluence-Seiten in Kategorien mit Unterseiten
#>
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
$SpaceKey = "BES"
$ParentPageId = "581013135" # Analyse pathOS Stammseite
$ApiBase = "$ConfluenceUrl/rest/api"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "============================================================"
Write-Host " Confluence Reorganisation - Analyse pathOS"
Write-Host "============================================================"
$PAT = $null
$SecretsPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\.secrets"
if (Test-Path $SecretsPath) {
Get-Content $SecretsPath | ForEach-Object {
if ($_ -match "^CONFLUENCE_TOKEN=(.+)$") {
$val = $Matches[1].Trim()
if ($val -ne "HIER_TOKEN_EINTRAGEN") { $PAT = $val }
}
}
}
if (-not $PAT) { $PAT = Read-Host "Confluence PAT" }
$Headers = @{ "Authorization" = "Bearer $PAT"; "Content-Type" = "application/json" }
function Invoke-ConfApi($Method, $Uri, $JsonBody) {
try {
if ($JsonBody) {
$Resp = Invoke-WebRequest -Uri $Uri -Method $Method -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($JsonBody)) -UseBasicParsing -ErrorAction Stop
} else {
$Resp = Invoke-WebRequest -Uri $Uri -Method $Method -Headers $Headers -UseBasicParsing -ErrorAction Stop
}
return ($Resp.Content | ConvertFrom-Json)
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
return $null
}
}
function Create-CategoryPage($Title, $ParentId, $Body) {
# Pruefen ob existiert
$SearchUri = "$ApiBase/content?spaceKey=$SpaceKey&title=$([Uri]::EscapeDataString($Title))&type=page"
$Search = Invoke-ConfApi "Get" $SearchUri
if ($Search.results -and $Search.results.Count -gt 0) {
Write-Host " Existiert bereits (ID: $($Search.results[0].id))" -ForegroundColor DarkGray
return $Search.results[0].id
}
$Json = @{
type = "page"
title = $Title
space = @{ key = $SpaceKey }
ancestors = @(@{ id = $ParentId })
body = @{ storage = @{ value = $Body; representation = "storage" } }
} | ConvertTo-Json -Depth 10
$Result = Invoke-ConfApi "Post" "$ApiBase/content" $Json
if ($Result) {
Write-Host " Erstellt (ID: $($Result.id))" -ForegroundColor Green
return $Result.id
}
return $null
}
function Move-Page($PageId, $NewParentId) {
# Aktuelle Version und Parent holen
$Page = Invoke-ConfApi "Get" "$ApiBase/content/$PageId`?expand=version,ancestors"
if (-not $Page) { return $false }
# Pruefen ob schon unter richtigem Parent
if ($Page.ancestors -and $Page.ancestors.Count -gt 0) {
$CurrentParent = $Page.ancestors[-1].id
if ($CurrentParent -eq $NewParentId) {
Write-Host " Bereits korrekt" -ForegroundColor DarkGray
return $true
}
}
$Version = $Page.version.number + 1
$Title = $Page.title
$Json = @{
type = "page"
title = $Title
version = @{ number = $Version }
ancestors = @(@{ id = $NewParentId })
} | ConvertTo-Json -Depth 10
$Result = Invoke-ConfApi "Put" "$ApiBase/content/$PageId" $Json
if ($Result) { return $true }
return $false
}
# === Bekannte Seiten-IDs (aus letztem Push) ===
$Pages = @{
"summary" = "581019325" # 0. Management Summary
"uebersicht" = "581013185" # 1. Uebersicht
"probleme" = "581013186" # 2. Problemfelder
"roadmap2026" = "581013187" # 3. Technische Roadmap 2026
"gitlab" = "581013188" # 4. GitLab
"glossar" = "581013189" # 5. Abkuerzungen
"architektur" = "581013355" # 6. Architekturskizze
"fortschritt" = "581013356" # 7. Projektfortschritt
"links" = "581013548" # 8. Links
"journey" = "581013549" # 9. Customer Journey
"deepdive" = "581015678" # 10. Tech Deep Dive
"teamworkflow" = "581015844" # 11. Team Analyse
"aktionsplan" = "581016065" # 12. Roadmap Aktionsplan
"jira" = "581016316" # 13. Jira
"next" = "581019221" # 14. Naechste Schritte
"ttt" = "581019444" # 15. TTT/NEP2
"roadmapfull" = "581019652" # 16. Roadmap uebergreifend
"ttti" = "581023872" # 17. TTTI Deep Dive
"velocity" = "581024118" # 18. Velocity
}
# Dynamisch: SonarQube-Seite per Titel suchen (ID noch unbekannt)
$SonarSearch = Invoke-ConfApi "Get" "$ApiBase/content?spaceKey=$SpaceKey&title=$([Uri]::EscapeDataString('19. SonarQube Code-Qualitaet'))&type=page"
if ($SonarSearch -and $SonarSearch.results -and $SonarSearch.results.Count -gt 0) {
$Pages["sonarqube"] = $SonarSearch.results[0].id
Write-Host " SonarQube-Seite gefunden: $($Pages['sonarqube'])" -ForegroundColor DarkGray
}
# === Test Verbindung ===
Write-Host ""
Write-Host ">> Teste Verbindung..." -ForegroundColor Yellow
$Test = Invoke-ConfApi "Get" "$ApiBase/content/$ParentPageId"
if (-not $Test) { Write-Host "Verbindung fehlgeschlagen!" -ForegroundColor Red; exit 1 }
Write-Host " OK: $($Test.title)" -ForegroundColor Green
# === Schritt 1: Kategorie-Seiten erstellen ===
Write-Host ""
Write-Host ">> Erstelle Kategorie-Seiten..." -ForegroundColor Yellow
Write-Host " Systemueberblick..." -NoNewline
$CatSystem = Create-CategoryPage "Systemueberblick" $ParentPageId '<h2>Systemueberblick</h2><p>Architektur, Komponenten und Schnittstellen von pathOS.</p><h3>Unterseiten</h3><table><tr><th>Seite</th><th>Inhalt</th></tr><tr><td><strong>Uebersicht und Gesamtbild</strong></td><td>Zahlen auf einen Blick: 167 Projekte, 15 Kern-Services, Tech-Stack, Teams</td></tr><tr><td><strong>Architekturskizze</strong></td><td>Alle Komponenten und 15+ externe Schnittstellen als Tabelle mit Farbkodierung</td></tr><tr><td><strong>Customer Journey</strong></td><td>Happy Path Trassenbestellung: 7 Schritte aus Kundensicht mit Datenfluss durch alle IT-Systeme</td></tr><tr><td><strong>GitLab Projektlandschaft</strong></td><td>167 Projekte in 9 Gruppen, Sprachen-Verteilung, Aktivitaets-Status</td></tr><tr><td><strong>Abkuerzungsverzeichnis</strong></td><td>35+ Fachbegriffe und Abkuerzungen (TPN, BEP, IFP, PMW, etc.)</td></tr></table><ac:structured-macro ac:name="children"><ac:parameter ac:name="all">true</ac:parameter></ac:structured-macro>'
Write-Host " Technische Analyse..." -NoNewline
$CatTech = Create-CategoryPage "Technische Analyse" $ParentPageId '<h2>Technische Analyse</h2><p>Versionen, Dependencies, Code-Qualitaet und technische Roadmap.</p><h3>Unterseiten</h3><table><tr><th>Seite</th><th>Inhalt</th></tr><tr><td><strong>Technischer Deep Dive</strong></td><td>Gesundheits-Scorecard: Spring Boot 3.5 (Upgrade laeuft), Java 21, Camunda 8.8, CVE-Patches</td></tr><tr><td><strong>SonarQube Code-Qualitaet</strong></td><td>74 Projekte: Coverage, Bugs, Smells nach Team. Zero=Vorbild, CIB=SV-Schuld, tadef=0% Coverage</td></tr><tr><td><strong>Technische Roadmap 2026</strong></td><td>53 Items aus der internen Roadmap: 17 MUSS UKA, 24 MUSS 2026, 12 SOLL</td></tr><tr><td><strong>Nuetzliche Links</strong></td><td>GitLab, Confluence, Runbook, Portal-URLs, Kafka-UI, DefectDojo, oeffentliche Quellen</td></tr></table><ac:structured-macro ac:name="children"><ac:parameter ac:name="all">true</ac:parameter></ac:structured-macro>'
Write-Host " Teams und Velocity..." -NoNewline
$CatTeams = Create-CategoryPage "Teams und Velocity" $ParentPageId '<h2>Teams und Velocity</h2><p>Team-Struktur, Jira-Analyse, Durchsatz und Personenanalyse.</p><h3>Unterseiten</h3><table><tr><th>Seite</th><th>Inhalt</th></tr><tr><td><strong>Team und Workflow Analyse</strong></td><td>6 Teams (Zero, 404, CIB, OPs, BSSUPPORT, FbF), 5 Engpaesse, 9 Reorganisations-Empfehlungen</td></tr><tr><td><strong>Velocity und Team-Analyse</strong></td><td>12-Monats-Velocity, Go-Live-Effekt, Bug-Rate 10%&#8594;29%&#8594;20%, MVPs und Schwachstellen pro Person</td></tr><tr><td><strong>Jira-Analyse</strong></td><td>3845 Issues ueber 10 Boards, Backlog-Verteilung, Team-Auslastung</td></tr><tr><td><strong>Personal- und Bug-Analyse</strong></td><td>Rollen, Tenure, Bug-Korrelation pro Entwickler. SPOFs, Risiko-Matrix, Top 5 Massnahmen</td></tr></table><ac:structured-macro ac:name="children"><ac:parameter ac:name="all">true</ac:parameter></ac:structured-macro>'
Write-Host " TTT-Programm..." -NoNewline
$CatTTT = Create-CategoryPage "TTT-Programm" $ParentPageId '<h2>TTT-Programm</h2><p>TAF/TAP TSI Programmkontext, NEP2, TTTI-Integration und Problemfelder.</p><h3>Unterseiten</h3><table><tr><th>Seite</th><th>Inhalt</th></tr><tr><td><strong>TTT-Programm und NEP2</strong></td><td>Go-Live Dez 2025, Go/No-Go Sep 2025 positiv, NEP2 stabil, Programmrisiken TTTSOL-42 bis -52</td></tr><tr><td><strong>TTTI Deep Dive</strong></td><td>1000 Issues, aber nur 42 echte Bugs. Kritischste nach Bereich (pathOS, TPN, ujBau, Abrechnung)</td></tr><tr><td><strong>Identifizierte Problemfelder</strong></td><td>5 Hauptprobleme: Deployment-Komplexitaet, Dokumentation, Tech Debt, Altsystem, Teams</td></tr></table><ac:structured-macro ac:name="children"><ac:parameter ac:name="all">true</ac:parameter></ac:structured-macro>'
Write-Host " Roadmap und Planung..." -NoNewline
$CatRoadmap = Create-CategoryPage "Roadmap und Planung" $ParentPageId '<h2>Roadmap und Planung</h2><p>Priorisierte Aktionen, Zeitplaene und naechste Schritte.</p><h3>Unterseiten</h3><table><tr><th>Seite</th><th>Inhalt</th></tr><tr><td><strong>Roadmap uebergreifend und pathOS</strong></td><td>4 Phasen bis Mitte 2027 mit grafischem Team-Zeitstrahl: Stabilisieren, Haerten, Skalieren, Weiterentwickeln</td></tr><tr><td><strong>Roadmap und Aktionsplan</strong></td><td>24 priorisierte Aktionen (SOFORT/KURZ/MITTEL/LANG) mit Risiko-Matrix</td></tr><tr><td><strong>Produkt-Roadmap (konsolidiert)</strong></td><td>PO-Input aller 3 Teams + BO-Sicht. 18 neue Themen, 6 strategische Fragen, 4 Phasen</td></tr><tr><td><strong>Naechste Schritte</strong></td><td>Offene Punkte, Datenquellen-Status, priorisierte naechste Schritte</td></tr><tr><td><strong>Projektfortschritt</strong></td><td>Alle Iterationen mit Tasks, Zeiten und Ergebnissen</td></tr></table><ac:structured-macro ac:name="children"><ac:parameter ac:name="all">true</ac:parameter></ac:structured-macro>'
# === Schritt 2: Seiten verschieben ===
Write-Host ""
Write-Host ">> Verschiebe Seiten in Kategorien..." -ForegroundColor Yellow
# Systemueberblick
$Moves = @(
@{ id = $Pages["uebersicht"]; parent = $CatSystem; name = "Uebersicht" }
@{ id = $Pages["architektur"]; parent = $CatSystem; name = "Architektur" }
@{ id = $Pages["journey"]; parent = $CatSystem; name = "Customer Journey" }
@{ id = $Pages["gitlab"]; parent = $CatSystem; name = "GitLab" }
@{ id = $Pages["glossar"]; parent = $CatSystem; name = "Glossar" }
# Technische Analyse
@{ id = $Pages["deepdive"]; parent = $CatTech; name = "Tech Deep Dive" }
@{ id = $Pages["roadmap2026"]; parent = $CatTech; name = "Roadmap 2026" }
@{ id = $Pages["links"]; parent = $CatTech; name = "Links" }
# Teams und Velocity
@{ id = $Pages["teamworkflow"]; parent = $CatTeams; name = "Team Analyse" }
@{ id = $Pages["velocity"]; parent = $CatTeams; name = "Velocity" }
@{ id = $Pages["jira"]; parent = $CatTeams; name = "Jira" }
# TTT-Programm
@{ id = $Pages["ttt"]; parent = $CatTTT; name = "TTT/NEP2" }
@{ id = $Pages["ttti"]; parent = $CatTTT; name = "TTTI" }
@{ id = $Pages["probleme"]; parent = $CatTTT; name = "Problemfelder" }
# Roadmap und Planung
@{ id = $Pages["roadmapfull"]; parent = $CatRoadmap; name = "Roadmap gesamt" }
@{ id = $Pages["aktionsplan"]; parent = $CatRoadmap; name = "Aktionsplan" }
@{ id = $Pages["next"]; parent = $CatRoadmap; name = "Naechste Schritte" }
@{ id = $Pages["fortschritt"]; parent = $CatRoadmap; name = "Fortschritt" }
)
if ($Pages["sonarqube"]) {
$Moves += @{ id = $Pages["sonarqube"]; parent = $CatTech; name = "SonarQube" }
}
# Dynamisch: Neuere Seiten per Titel suchen und einsortieren
$DynPages = @(
@{ title = "19a. SonarQube Smells Detail"; parent = "CatTech"; name = "Smells Detail" }
@{ title = "20. Geschaeftsprozesse"; parent = "CatSystem"; name = "Geschaeftsprozesse" }
@{ title = "21. Produkt-Roadmap (konsolidiert)"; parent = "CatRoadmap"; name = "Produkt-Roadmap" }
@{ title = "22. Personal- und Bug-Analyse"; parent = "CatTeams"; name = "Personal-Analyse" }
)
foreach ($DP in $DynPages) {
$DSearch = Invoke-ConfApi "Get" "$ApiBase/content?spaceKey=$SpaceKey&title=$([Uri]::EscapeDataString($DP.title))&type=page"
if ($DSearch -and $DSearch.results -and $DSearch.results.Count -gt 0) {
$TargetParent = switch ($DP.parent) {
"CatTech" { $CatTech }
"CatSystem" { $CatSystem }
"CatRoadmap" { $CatRoadmap }
"CatTeams" { $CatTeams }
"CatTTT" { $CatTTT }
}
$Moves += @{ id = $DSearch.results[0].id; parent = $TargetParent; name = $DP.name }
Write-Host " $($DP.name) gefunden (ID: $($DSearch.results[0].id))" -ForegroundColor DarkGray
} else {
Write-Host " $($DP.name) nicht gefunden (noch nicht gepusht?)" -ForegroundColor Yellow
}
}
foreach ($M in $Moves) {
Write-Host " $($M.name) -> Kategorie..." -NoNewline
$Ok = Move-Page $M.id $M.parent
if ($Ok) { Write-Host " OK" -ForegroundColor Green }
else { Write-Host " FEHLER" -ForegroundColor Red }
}
# Management Summary umbenennen - ENTFERNT (alte Seite bereits geloescht)
Write-Host " Management Summary: Bereits korrekt" -ForegroundColor DarkGray
# === Schritt 3: Stammseite aktualisieren ===
Write-Host ""
Write-Host ">> Stammseite aktualisieren..." -ForegroundColor Yellow
$StammPage = Invoke-ConfApi "Get" "$ApiBase/content/$ParentPageId`?expand=version"
$StammVersion = $StammPage.version.number + 1
$StammHtml = @"
<h2>Analyse pathOS &#8212; Portfolio-Audit</h2>
<p><strong>Business Owner:</strong> Andre Knie | <strong>Start:</strong> 2026-04-22 | <strong>Methodik:</strong> Iterativ</p>
<ac:structured-macro ac:name="info"><ac:rich-text-body>
<p>Systematische Analyse der pathOS-Plattform (~140 GitLab-Projekte, Confluence, Jira, Runbook). pathOS ist seit Dezember 2025 produktiv (FplJ 27). Aktuell: Verlaengerte Hypercare + NEP2.</p>
</ac:rich-text-body></ac:structured-macro>
<h3>Schnelleinstieg</h3>
<table>
<tr>
<td style="width:50%;vertical-align:top;padding:10px;">
<h4><ac:link><ri:page ri:content-title="0. Management Summary"/></ac:link></h4>
<p>Kompakte Zusammenfassung mit Zeitstrahl, Top-5 Risiken und Staerken.</p>
</td>
<td style="width:50%;vertical-align:top;padding:10px;">
<h4><ac:link><ri:page ri:content-title="Roadmap und Planung"/></ac:link></h4>
<p>Priorisierte Aktionen und Zeitplaene bis Mitte 2027.</p>
</td>
</tr>
</table>
<h3>Bereiche</h3>
<table>
<tr><th>Bereich</th><th>Inhalt</th></tr>
<tr><td><ac:link><ri:page ri:content-title="Systemueberblick"/></ac:link></td><td>Architektur, Komponenten, Customer Journey, GitLab-Landschaft, Glossar</td></tr>
<tr><td><ac:link><ri:page ri:content-title="Technische Analyse"/></ac:link></td><td>Versionen, Dependencies, Gesundheits-Scorecard, Technische Roadmap 2026</td></tr>
<tr><td><ac:link><ri:page ri:content-title="Teams und Velocity"/></ac:link></td><td>Team-Struktur, 12-Monats-Velocity, Jira-Analyse, MVPs und Schwachstellen</td></tr>
<tr><td><ac:link><ri:page ri:content-title="TTT-Programm"/></ac:link></td><td>TAF/TAP TSI Kontext, NEP2, TTTI-Integration, Problemfelder</td></tr>
<tr><td><ac:link><ri:page ri:content-title="Roadmap und Planung"/></ac:link></td><td>Aktionsplan (24 Massnahmen), Roadmap bis Mitte 2027, Naechste Schritte</td></tr>
</table>
<h3>Aktueller Status</h3>
<table>
<tr><th>Phase</th><th>Status</th></tr>
<tr style="background:#d5e8d4;"><td>Phase 1: Domain Discovery</td><td>&#9989; Abgeschlossen (80 Min)</td></tr>
<tr style="background:#d5e8d4;"><td>Phase 2: Technischer Deep Dive</td><td>&#9989; Abgeschlossen (55 Min)</td></tr>
<tr style="background:#d5e8d4;"><td>Phase 3: Team &amp; Workflow</td><td>&#9989; Abgeschlossen (30 Min)</td></tr>
<tr style="background:#d5e8d4;"><td>Phase 4: Roadmap &amp; Aktionsplan</td><td>&#9989; Abgeschlossen</td></tr>
<tr style="background:#d5e8d4;"><td>Velocity-Analyse (12 Monate)</td><td>&#9989; Abgeschlossen</td></tr>
<tr style="background:#d5e8d4;"><td>TTTI + TTTSol Deep Dive</td><td>&#9989; Abgeschlossen</td></tr>
</table>
"@
$StammJson = @{
version = @{ number = $StammVersion }
title = "Analyse pathOS"
type = "page"
body = @{ storage = @{ value = $StammHtml; representation = "storage" } }
} | ConvertTo-Json -Depth 10
$StammResult = Invoke-ConfApi "Put" "$ApiBase/content/$ParentPageId" $StammJson
if ($StammResult) { Write-Host " Stammseite aktualisiert" -ForegroundColor Green }
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " FERTIG! Seiten reorganisiert." -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
@@ -0,0 +1,183 @@
<#
.SYNOPSIS
Test: Pusht eine einzelne Seite um Umlaut-Konvertierung zu pruefen
#>
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
$SpaceKey = "BES"
$ParentPageId = "581013135"
$ApiBase = "$ConfluenceUrl/rest/api"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Token aus .secrets
$SecretsPath = Join-Path $ScriptDir "..\.secrets"
$PAT = $null
if (Test-Path $SecretsPath) {
Get-Content $SecretsPath | ForEach-Object {
if ($_ -match "^CONFLUENCE_TOKEN=(.+)$") {
$val = $Matches[1].Trim()
if ($val -ne "HIER_TOKEN_EINTRAGEN") { $PAT = $val }
}
}
}
if (-not $PAT) { $PAT = Read-Host "Confluence PAT" }
$Headers = @{ "Authorization" = "Bearer $PAT"; "Content-Type" = "application/json" }
# Fix-Umlauts Funktion (identisch zum Haupt-Script)
function Fix-Umlauts($html) {
if ($null -eq $html -or $html -eq "") { return $html }
$Protect = @(
'Israel','Michael','Raphael','Emmanuel','Manuel','Daniel','Samuel',
'Maurer','Enabler','enabler','Container','container','Cluster','cluster',
'User','user','Tracing','Staging','Logging','Alerting','Monitoring',
'Deployment','deployment','Security','security','Recovery','Discovery',
'Delivery','Pipeline','pipeline','Release','release',
'Performance','performance','Interface','interface',
'Service','service','Feature','feature','Issue','issue',
'value','true','blue','Due','due','Queue','queue',
'Unique','unique','Continue','continue',
'Funnel','Tunnel','tunnel','Buildpacks','Dockerfile',
'OpenSearch','Gherkin','Kubernetes','Helmfile','Renovate',
'Operate','Template','template','Tasklist',
'Smoke','smoke','Analyse','analyse','Analysis',
'Baseline','baseline','Namespace','namespace',
'Codebase','Database','database','Rescue','rescue'
)
foreach ($w in $Protect) { $html = $html.Replace($w, "PROTECT_$w") }
# Namen
$html = $html.Replace('Hoerpel', 'H&ouml;rpel')
$html = $html.Replace('Ruecker', 'R&uuml;cker')
$html = $html.Replace('Koehler', 'K&ouml;hler')
$html = $html.Replace('Goendoer', 'G&ouml;nd&ouml;r')
# ue
$html = $html.Replace('Ueber', '&Uuml;ber')
$html = $html.Replace('ueber', '&uuml;ber')
$html = $html.Replace('Pruef', 'Pr&uuml;f'); $html = $html.Replace('pruef', 'pr&uuml;f')
$html = $html.Replace('fuehrung', 'f&uuml;hrung'); $html = $html.Replace('Fuehrung', 'F&uuml;hrung')
$html = $html.Replace('Verfueg', 'Verf&uuml;g'); $html = $html.Replace('verfueg', 'verf&uuml;g')
$html = $html.Replace('Verschluess', 'Verschl&uuml;ss'); $html = $html.Replace('verschluess', 'verschl&uuml;ss')
$html = $html.Replace('Schluess', 'Schl&uuml;ss'); $html = $html.Replace('schluess', 'schl&uuml;ss')
$html = $html.Replace('Verknuepf', 'Verkn&uuml;pf'); $html = $html.Replace('verknuepf', 'verkn&uuml;pf')
$html = $html.Replace('Unterstuetz', 'Unterst&uuml;tz'); $html = $html.Replace('unterstuetz', 'unterst&uuml;tz')
$html = $html.Replace('Luecke', 'L&uuml;cke'); $html = $html.Replace('luecke', 'l&uuml;cke')
$html = $html.Replace('Rueck', 'R&uuml;ck'); $html = $html.Replace('rueck', 'r&uuml;ck')
$html = $html.Replace('Frueh', 'Fr&uuml;h'); $html = $html.Replace('frueh', 'fr&uuml;h')
$html = $html.Replace('Nuetzlich', 'N&uuml;tzlich'); $html = $html.Replace('nuetzlich', 'n&uuml;tzlich')
$html = $html.Replace('Abkuerz', 'Abk&uuml;rz'); $html = $html.Replace('abkuerz', 'abk&uuml;rz')
$html = $html.Replace('wuerde', 'w&uuml;rde'); $html = $html.Replace('Wuerde', 'W&uuml;rde')
$html = $html.Replace('muessen', 'm&uuml;ssen'); $html = $html.Replace('Muessen', 'M&uuml;ssen')
$html = $html.Replace('muesste', 'm&uuml;sste'); $html = $html.Replace('Muesste', 'M&uuml;sste')
$html = $html.Replace('koennte', 'k&ouml;nnte'); $html = $html.Replace('Koennte', 'K&ouml;nnte')
$html = $html.Replace('fuer ', 'f&uuml;r '); $html = $html.Replace('Fuer ', 'F&uuml;r ')
# oe
$html = $html.Replace('Loesung', 'L&ouml;sung'); $html = $html.Replace('loesung', 'l&ouml;sung')
$html = $html.Replace('Erhoeh', 'Erh&ouml;h'); $html = $html.Replace('erhoeh', 'erh&ouml;h')
$html = $html.Replace('noetig', 'n&ouml;tig'); $html = $html.Replace('Noetig', 'N&ouml;tig')
$html = $html.Replace('moeglich', 'm&ouml;glich'); $html = $html.Replace('Moeglich', 'M&ouml;glich')
$html = $html.Replace('geloest', 'gel&ouml;st')
$html = $html.Replace('hoechst', 'h&ouml;chst'); $html = $html.Replace('Hoechst', 'H&ouml;chst')
$html = $html.Replace('koennen', 'k&ouml;nnen'); $html = $html.Replace('Koennen', 'K&ouml;nnen')
$html = $html.Replace('Zugehoer', 'Zugeh&ouml;r'); $html = $html.Replace('zugehoer', 'zugeh&ouml;r')
$html = $html.Replace('gehoer', 'geh&ouml;r')
# ae
$html = $html.Replace('Aenderung', '&Auml;nderung'); $html = $html.Replace('aenderung', '&auml;nderung')
$html = $html.Replace('anfaellig', 'anf&auml;llig'); $html = $html.Replace('faellig', 'f&auml;llig')
$html = $html.Replace('faehig', 'f&auml;hig')
$html = $html.Replace('traeger', 'tr&auml;ger'); $html = $html.Replace('Traeger', 'Tr&auml;ger')
$html = $html.Replace('laenger', 'l&auml;nger'); $html = $html.Replace('Laenger', 'L&auml;nger')
$html = $html.Replace('erklaer', 'erkl&auml;r')
$html = $html.Replace('klaer', 'kl&auml;r'); $html = $html.Replace('Klaer', 'Kl&auml;r')
$html = $html.Replace('verstaerk', 'verst&auml;rk')
$html = $html.Replace('Verstaend', 'Verst&auml;nd'); $html = $html.Replace('verstaend', 'verst&auml;nd')
$html = $html.Replace('Abhaengig', 'Abh&auml;ngig'); $html = $html.Replace('abhaengig', 'abh&auml;ngig')
$html = $html.Replace('haeufig', 'h&auml;ufig'); $html = $html.Replace('Haeufig', 'H&auml;ufig')
$html = $html.Replace('laeuft', 'l&auml;uft'); $html = $html.Replace('Laeuft', 'L&auml;uft')
$html = $html.Replace('waere', 'w&auml;re'); $html = $html.Replace('Waere', 'W&auml;re')
$html = $html.Replace('schaerfen', 'sch&auml;rfen'); $html = $html.Replace('Schaerfen', 'Sch&auml;rfen')
$html = $html.Replace('aufraeumen', 'aufr&auml;umen'); $html = $html.Replace('Aufraeumen', 'Aufr&auml;umen')
$html = $html.Replace('Verlaenger', 'Verl&auml;nger'); $html = $html.Replace('verlaenger', 'verl&auml;nger')
$html = $html.Replace('Naechst', 'N&auml;chst'); $html = $html.Replace('naechst', 'n&auml;chst')
$html = $html.Replace('Massnahme', 'Ma&szlig;nahme'); $html = $html.Replace('massnahme', 'ma&szlig;nahme')
$html = $html.Replace('Groesse', 'Gr&ouml;&szlig;e'); $html = $html.Replace('groesse', 'gr&ouml;&szlig;e')
$html = $html.Replace('gleichmaessig', 'gleichm&auml;&szlig;ig')
$html = $html -replace '([a-z])taet\b', '$1t&auml;t'
# Schutz aufheben
$html = $html.Replace('PROTECT_', '')
return $html
}
# Fuer Seitentitel: Echte Unicode-Zeichen (PS5.1 kompatibel via Char-Codes)
function Fix-Title($title) {
$title = $title.Replace('ue', "$([char]0xFC)")
$title = $title.Replace('Ue', "$([char]0xDC)")
$title = $title.Replace('oe', "$([char]0xF6)")
$title = $title.Replace('Oe', "$([char]0xD6)")
$title = $title.Replace('ae', "$([char]0xE4)")
$title = $title.Replace('Ae', "$([char]0xC4)")
return $title
}
# Test-HTML mit vielen Umlauten
$TestHtml = @"
<h2>Umlaut-Test</h2>
<p>Ueberblick ueber die Qualitaet: Hoechste Prioritaet fuer Massnahmen.</p>
<p>Das Team muesste die Komplexitaet verstaendlich erklaeren koennen.</p>
<table>
<tr><th>Person</th><th>Rolle</th><th>Bewertung</th></tr>
<tr><td>Leon Hoerpel</td><td>Dev</td><td>Bug-anfaellig</td></tr>
<tr><td>Dominik Ruecker</td><td>Backend Dev</td><td>Verfuegbar</td></tr>
<tr><td>Jonas Koehler</td><td>Dev</td><td>Leistungstraeger</td></tr>
<tr><td>Sebastian Goendoer</td><td>Dev (ehem. STeam)</td><td>Wenig aktiv</td></tr>
<tr><td>Emmanuel Kontcheu Tagne</td><td>Frontend Dev</td><td>KRITISCH</td></tr>
</table>
<p>Naechste Schritte: Abhaengigkeiten klaeren, Loesungen erhoehen, Verschluesselung pruefen.</p>
"@
# Konvertieren und anzeigen
$Converted = Fix-Umlauts $TestHtml
Write-Host "=== VORHER (Auszug) ===" -ForegroundColor Yellow
Write-Host " Ueberblick, Qualitaet, Hoerpel, Goendoer, Emmanuel"
Write-Host ""
Write-Host "=== NACHHER (Auszug) ===" -ForegroundColor Green
# Zeige ein paar konvertierte Stellen
if ($Converted -match '&Uuml;berblick') { Write-Host " Ueberblick -> OK" -ForegroundColor Green }
if ($Converted -match 'Qualit&auml;t') { Write-Host " Qualitaet -> OK" -ForegroundColor Green }
if ($Converted -match 'H&ouml;rpel') { Write-Host " Hoerpel -> OK" -ForegroundColor Green }
if ($Converted -match 'G&ouml;nd&ouml;r') { Write-Host " Goendoer -> OK" -ForegroundColor Green }
if ($Converted -match 'Emmanuel') { Write-Host " Emmanuel -> OK (nicht konvertiert)" -ForegroundColor Green }
if ($Converted -match 'Ma&szlig;nahme') { Write-Host " Massnahme -> OK" -ForegroundColor Green }
if ($Converted -match 'f&uuml;r ') { Write-Host " fuer -> OK" -ForegroundColor Green }
if ($Converted -match 'Abh&auml;ngig') { Write-Host " Abhaengig -> OK" -ForegroundColor Green }
if ($Converted -match 'kl&auml;r') { Write-Host " klaeren -> OK" -ForegroundColor Green }
if ($Converted -match 'Komplexit&auml;t') { Write-Host " Komplexitaet -> OK" -ForegroundColor Green }
# Pruefen ob Emmanuel NICHT kaputt ist
if ($Converted -match 'E&') { Write-Host " FEHLER: Emmanuel wurde konvertiert!" -ForegroundColor Red }
Write-Host ""
Write-Host ">> Pushe Test-Seite nach Confluence..." -ForegroundColor Yellow
# Push
$SearchUri = "$ApiBase/content?spaceKey=$SpaceKey&title=$([Uri]::EscapeDataString('TEST Umlaut-Pruefung'))&type=page"
$SearchResp = Invoke-WebRequest -Uri $SearchUri -Headers $Headers -UseBasicParsing -ErrorAction Stop
$SearchData = $SearchResp.Content | ConvertFrom-Json
if ($SearchData.results -and $SearchData.results.Count -gt 0) {
$PageId = $SearchData.results[0].id
$VerResp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId`?expand=version" -Headers $Headers -UseBasicParsing
$Version = ($VerResp.Content | ConvertFrom-Json).version.number + 1
$Body = @{ version = @{ number = $Version }; title = "TEST Umlaut-Pruefung"; type = "page"; body = @{ storage = @{ value = $Converted; representation = "storage" } } } | ConvertTo-Json -Depth 10
$Resp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId" -Method Put -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($Body)) -UseBasicParsing
Write-Host " Aktualisiert: $ConfluenceUrl/pages/viewpage.action?pageId=$PageId" -ForegroundColor Green
} else {
$Body = @{ type = "page"; title = "TEST Umlaut-Pruefung"; space = @{ key = $SpaceKey }; ancestors = @(@{ id = $ParentPageId }); body = @{ storage = @{ value = $Converted; representation = "storage" } } } | ConvertTo-Json -Depth 10
$Resp = Invoke-WebRequest -Uri "$ApiBase/content" -Method Post -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($Body)) -UseBasicParsing
$NewId = ($Resp.Content | ConvertFrom-Json).id
Write-Host " Erstellt: $ConfluenceUrl/pages/viewpage.action?pageId=$NewId" -ForegroundColor Green
}
Write-Host ""
Write-Host "Pruefe die Seite in Confluence und sag mir ob die Umlaute stimmen!" -ForegroundColor Cyan
@@ -0,0 +1,146 @@
<#
.SYNOPSIS
Zieht pom.xml und package.json der Kern-Services fuer Versionsanalyse
#>
$GitLabUrl = "https://git.tech.rz.db.de"
$ApiBase = "$GitLabUrl/api/v4"
$OutputDir = "project-audit\data\pom-analysis"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$Token = Read-Host "GitLab Token (read_api)"
$Headers = @{ "PRIVATE-TOKEN" = $Token }
# Kern-Services (apps/ + libraries/ + ausgewaehlte apis/)
$Projects = @(
@{ path = "bestellsystem1/apps/steuerung-vertrieb"; name = "steuerung-vertrieb" }
@{ path = "bestellsystem1/apps/auftrags-verwaltung-trasse"; name = "auftrags-verwaltung-trasse" }
@{ path = "bestellsystem1/apps/portal-middleware"; name = "portal-middleware" }
@{ path = "bestellsystem1/apps/portal-ui"; name = "portal-ui" }
@{ path = "bestellsystem1/apps/common-interface"; name = "common-interface" }
@{ path = "bestellsystem1/apps/stammdaten-bereitstellung"; name = "stammdaten-bereitstellung" }
@{ path = "bestellsystem1/apps/kundendaten-bereitstellung"; name = "kundendaten-bereitstellung" }
@{ path = "bestellsystem1/apps/ifp-connector"; name = "ifp-connector" }
@{ path = "bestellsystem1/apps/taftap-tdm-konverter"; name = "taftap-tdm-konverter" }
@{ path = "bestellsystem1/apps/vertragsdaten-verteiler"; name = "vertragsdaten-verteiler" }
@{ path = "bestellsystem1/apps/archivierungsservice"; name = "archivierungsservice" }
@{ path = "bestellsystem1/apps/rabattnummern-bereitstellung"; name = "rabattnummern-bereitstellung" }
@{ path = "bestellsystem1/apps/tbv-absicherung-konverter"; name = "tbv-absicherung-konverter" }
@{ path = "bestellsystem1/apps/tadef-connector"; name = "tadef-connector" }
@{ path = "bestellsystem1/apps/auftrag-service"; name = "auftrag-service" }
@{ path = "bestellsystem1/apps/bestellsystem-parent-pom"; name = "bestellsystem-parent-pom" }
@{ path = "bestellsystem1/libraries/core-components-common"; name = "core-components-common" }
@{ path = "bestellsystem1/libraries/core-components-kafka"; name = "core-components-kafka" }
@{ path = "bestellsystem1/libraries/core-components-test"; name = "core-components-test" }
@{ path = "bestellsystem1/libraries/logging-utils"; name = "logging-utils" }
@{ path = "bestellsystem1/libraries/signature-database"; name = "signature-database" }
@{ path = "bestellsystem1/libraries/signature-message"; name = "signature-message" }
@{ path = "bestellsystem1/apis/data-model"; name = "data-model-api" }
)
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
Write-Host "Lade pom.xml / package.json fuer $($Projects.Count) Projekte..."
foreach ($Proj in $Projects) {
$Encoded = [Uri]::EscapeDataString($Proj.path)
$Name = $Proj.name
Write-Host " $Name..." -NoNewline
# pom.xml versuchen (main, dann master)
$Found = $false
foreach ($Branch in @("main", "master")) {
foreach ($File in @("pom.xml", "package.json")) {
$Uri = "$ApiBase/projects/$Encoded/repository/files/$([Uri]::EscapeDataString($File))/raw?ref=$Branch"
try {
$Resp = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
$OutPath = Join-Path $OutputDir "$Name`_$File"
$Resp.Content | Out-File -FilePath $OutPath -Encoding UTF8
Write-Host " $File ($Branch)" -ForegroundColor Green
$Found = $true
break
} catch { continue }
}
if ($Found) { break }
}
if (-not $Found) { Write-Host " nicht gefunden" -ForegroundColor Yellow }
}
# Analyse: Spring Boot + Java Versionen extrahieren
Write-Host ""
Write-Host ">> Analysiere Versionen..." -ForegroundColor Yellow
$Results = @()
$PomFiles = Get-ChildItem $OutputDir -Filter "*_pom.xml"
foreach ($Pom in $PomFiles) {
$Name = $Pom.Name -replace "_pom.xml", ""
$Content = Get-Content $Pom.FullName -Raw
$SpringBoot = ""
$Java = ""
$Parent = ""
$Camunda = ""
# Spring Boot Version
if ($Content -match "spring-boot-starter-parent.*?<version>(.*?)</version>") { $SpringBoot = $Matches[1] }
if ($Content -match "spring\.boot\.version.*?>(.*?)<") { $SpringBoot = $Matches[1] }
if ($Content -match "spring-boot\.version.*?>(.*?)<") { $SpringBoot = $Matches[1] }
# Java Version
if ($Content -match "<java\.version>(.*?)</java\.version>") { $Java = $Matches[1] }
if ($Content -match "<maven\.compiler\.source>(.*?)</maven\.compiler\.source>") { $Java = $Matches[1] }
# Parent POM
if ($Content -match "<parent>.*?<artifactId>(.*?)</artifactId>.*?<version>(.*?)</version>.*?</parent>") {
$Parent = "$($Matches[1]):$($Matches[2])"
}
# Camunda
if ($Content -match "camunda.*?<version>(.*?)</version>") { $Camunda = $Matches[1] }
$Results += [PSCustomObject]@{
Service = $Name
SpringBoot = $SpringBoot
Java = $Java
Parent = $Parent
Camunda = $Camunda
}
}
# Package.json analysieren
$PkgFiles = Get-ChildItem $OutputDir -Filter "*_package.json"
foreach ($Pkg in $PkgFiles) {
$Name = $Pkg.Name -replace "_package.json", ""
$Content = Get-Content $Pkg.FullName -Raw | ConvertFrom-Json
$Angular = ""
if ($Content.dependencies -and $Content.dependencies.'@angular/core') {
$Angular = $Content.dependencies.'@angular/core'
}
$Results += [PSCustomObject]@{
Service = $Name
SpringBoot = ""
Java = ""
Parent = ""
Camunda = ""
Angular = $Angular
NodeVersion = if ($Content.engines -and $Content.engines.node) { $Content.engines.node } else { "" }
}
}
# Ergebnis speichern
$ResultPath = Join-Path $OutputDir "version-analysis.md"
$Md = @("# Versionsanalyse pathOS Kern-Services", "", "| Service | Spring Boot | Java | Parent POM | Camunda |")
$Md += "|---------|------------|------|------------|---------|"
foreach ($R in $Results) {
$Md += "| $($R.Service) | $($R.SpringBoot) | $($R.Java) | $($R.Parent) | $($R.Camunda) |"
}
$Md -join "`n" | Out-File -FilePath $ResultPath -Encoding UTF8
$Results | Export-Csv -Path (Join-Path $OutputDir "version-analysis.csv") -Delimiter ";" -Encoding UTF8 -NoTypeInformation
Write-Host ""
Write-Host "FERTIG! Ergebnisse in $OutputDir" -ForegroundColor Green
$Results | Format-Table -AutoSize
+102
View File
@@ -0,0 +1,102 @@
"""
Git-Init fuer project-audit (nutzt dulwich statt git binary)
Initialisiert Repo, staged alle Dateien, erstellt Initial-Commit.
"""
import os
import sys
from pathlib import Path
# dulwich imports
from dulwich.repo import Repo
from dulwich.objects import Blob, Tree, Commit
from dulwich import porcelain
REPO_PATH = Path(__file__).parent.parent # project-audit/
REMOTE_URL = "https://git.tech.rz.db.de/AndreKnie/project-audit.git"
def main():
print("=== Git Init: project-audit ===")
print(f" Pfad: {REPO_PATH}")
# Repo initialisieren
if (REPO_PATH / ".git").exists():
print(" Repo existiert bereits.")
repo = Repo(str(REPO_PATH))
else:
print(" Initialisiere neues Repo...")
repo = Repo.init(str(REPO_PATH))
print(" OK")
# .gitignore lesen fuer Ausschluesse
ignore_patterns = []
gitignore_path = REPO_PATH / ".gitignore"
if gitignore_path.exists():
with open(gitignore_path, "r") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#"):
ignore_patterns.append(line)
def should_ignore(rel_path):
"""Einfacher .gitignore Check."""
rel_str = str(rel_path).replace("\\", "/")
for pattern in ignore_patterns:
pattern_clean = pattern.strip("/")
if pattern_clean in rel_str:
return True
if rel_str.endswith(pattern_clean):
return True
return False
# Alle Dateien stagen
print(" Stage Dateien...")
staged = 0
for root, dirs, files in os.walk(REPO_PATH):
# .git Verzeichnis ueberspringen
if ".git" in root:
continue
for fname in files:
full_path = Path(root) / fname
rel_path = full_path.relative_to(REPO_PATH)
if should_ignore(rel_path):
continue
try:
porcelain.add(str(REPO_PATH), paths=[str(rel_path)])
staged += 1
except Exception:
pass
print(f" {staged} Dateien gestaged")
# Commit
print(" Erstelle Initial-Commit...")
try:
porcelain.commit(
str(REPO_PATH),
message=b"Initial: pathOS Portfolio-Audit (4 Tage Analyse)",
author=b"Andre Knie <andre.knie@deutschebahn.com>",
committer=b"Andre Knie <andre.knie@deutschebahn.com>"
)
print(" Commit erstellt!")
except Exception as e:
print(f" Commit-Fehler: {e}")
# Remote hinzufuegen
print(f" Remote: {REMOTE_URL}")
try:
config = repo.get_config()
config.set((b"remote", b"origin"), b"url", REMOTE_URL.encode())
config.set((b"remote", b"origin"), b"fetch", b"+refs/heads/*:refs/remotes/origin/*")
config.write_to_path()
print(" Remote 'origin' konfiguriert")
except Exception as e:
print(f" Remote-Fehler: {e}")
print("\n FERTIG!")
print(f" Push spaeter mit: python -m dulwich push {REMOTE_URL}")
print(f" Oder wenn git installiert: git push -u origin main")
if __name__ == "__main__":
main()
+192
View File
@@ -0,0 +1,192 @@
"""
Git Push via GitLab Commits API (REST).
Umgeht das Git-HTTP-Protokoll komplett.
Nutzt POST /api/v4/projects/:id/repository/commits
Strategie: Vergleiche lokale Dateien mit Remote, pushe nur Aenderungen.
"""
import urllib.request, json, ssl, base64, os
from pathlib import Path
REPO_PATH = Path(__file__).parent.parent # project-audit/
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
TOKEN = secrets.get("GITLAB_TOKEN_AUDIT", "")
PROJECT = "AndreKnie%2Fproject-audit"
BASE = f"https://git.tech.rz.db.de/api/v4/projects/{PROJECT}"
BRANCH = "master"
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
def api_get(path):
url = BASE + path
req = urllib.request.Request(url)
req.add_header("PRIVATE-TOKEN", TOKEN)
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
return json.loads(resp.read().decode("utf-8"))
def api_post(path, data):
url = BASE + path
body = json.dumps(data).encode("utf-8")
req = urllib.request.Request(url, data=body, method="POST")
req.add_header("PRIVATE-TOKEN", TOKEN)
req.add_header("Content-Type", "application/json")
resp = urllib.request.urlopen(req, context=ctx, timeout=60)
return json.loads(resp.read().decode("utf-8"))
def get_remote_tree(path="", ref=BRANCH):
"""Holt den Dateibaum vom Remote rekursiv."""
files = {}
page = 1
while True:
try:
items = api_get(f"/repository/tree?ref={ref}&path={path}&per_page=100&page={page}&recursive=true")
if not items:
break
for item in items:
if item["type"] == "blob":
files[item["path"]] = item["id"] # SHA des Blobs
page += 1
except Exception as e:
print(f" Tree-Fehler (Seite {page}): {e}")
break
return files
def get_local_files():
"""Sammelt alle lokalen Dateien (respektiert .gitignore)."""
patterns = []
gitignore = REPO_PATH / ".gitignore"
if gitignore.exists():
with open(gitignore) as f:
patterns = [l.strip().strip("/") for l in f if l.strip() and not l.startswith("#")]
files = {}
for root, dirs, fnames in os.walk(REPO_PATH):
if ".git" in root:
continue
for fname in fnames:
full = Path(root) / fname
rel = str(full.relative_to(REPO_PATH)).replace("\\", "/")
if any(p in rel for p in patterns):
continue
files[rel] = full
return files
def main():
print(f"=== Git Push via API ===")
print(f" Token: ...{TOKEN[-4:]}")
print(f" Branch: {BRANCH}")
print()
# 1. Remote-Tree holen
print(" Remote-Tree laden...")
remote_files = get_remote_tree()
print(f" Remote: {len(remote_files)} Dateien")
# 2. Lokale Dateien sammeln
print(" Lokale Dateien sammeln...")
local_files = get_local_files()
print(f" Lokal: {len(local_files)} Dateien")
# 3. Diff berechnen
actions = []
new_files = set(local_files.keys()) - set(remote_files.keys())
deleted_files = set(remote_files.keys()) - set(local_files.keys())
# Neue Dateien
for f in sorted(new_files):
path = local_files[f]
try:
content = path.read_bytes()
actions.append({
"action": "create",
"file_path": f,
"content": base64.b64encode(content).decode(),
"encoding": "base64"
})
except Exception:
pass
# Geaenderte Dateien (vereinfacht: alle existierenden neu hochladen)
# Nur wenn es neue/geloeschte gibt, sonst nichts zu tun
changed = []
for f in sorted(set(local_files.keys()) & set(remote_files.keys())):
path = local_files[f]
try:
content = path.read_bytes()
# Einfacher Check: Dateigroesse oder Inhalt hat sich geaendert
# Wir koennen den SHA nicht lokal berechnen ohne git, also updaten wir alles
# was in den letzten Tagen geaendert wurde
mtime = os.path.getmtime(path)
import time
if time.time() - mtime < 7 * 86400: # Letzte 7 Tage
changed.append(f)
actions.append({
"action": "update",
"file_path": f,
"content": base64.b64encode(content).decode(),
"encoding": "base64"
})
except Exception:
pass
# Geloeschte Dateien
for f in sorted(deleted_files):
actions.append({
"action": "delete",
"file_path": f
})
print(f" Neu: {len(new_files)}, Geaendert: {len(changed)}, Geloescht: {len(deleted_files)}")
if not actions:
print(" Keine Aenderungen zu pushen.")
return
# GitLab API hat ein Limit von ~100 Actions pro Commit
# Aufteilen in Batches
BATCH_SIZE = 50
total_batches = (len(actions) + BATCH_SIZE - 1) // BATCH_SIZE
print(f" Push in {total_batches} Batch(es) a {BATCH_SIZE} Dateien...")
for i in range(0, len(actions), BATCH_SIZE):
batch = actions[i:i+BATCH_SIZE]
batch_num = i // BATCH_SIZE + 1
commit_msg = f"Audit Update (Batch {batch_num}/{total_batches})" if total_batches > 1 else "Audit Update"
print(f" Batch {batch_num}: {len(batch)} Aktionen...")
try:
result = api_post("/repository/commits", {
"branch": BRANCH,
"commit_message": commit_msg,
"actions": batch
})
print(f" OK: {result.get('short_id', '?')} - {result.get('title', '?')}")
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
print(f" FEHLER {e.code}: {body[:300]}")
if e.code == 400 and "already exists" in body:
print(" Tipp: Datei existiert bereits, versuche 'update' statt 'create'")
break
except Exception as e:
print(f" FEHLER: {e}")
break
print()
print(" Fertig!")
if __name__ == "__main__":
main()
@@ -0,0 +1,32 @@
"""Nur Push (kein Stage/Commit) - testet ob oauth2-Auth den Push ermoeglicht."""
from pathlib import Path
from dulwich import porcelain
REPO_PATH = str(Path(__file__).parent.parent)
REMOTE_URL = "https://git.tech.rz.db.de/AndreKnie/project-audit.git"
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
token = secrets.get("GITLAB_TOKEN_AUDIT", "")
print(f"Push mit oauth2-Auth (Token: ...{token[-4:]})")
print(f"Repo: {REPO_PATH}")
print(f"Remote: {REMOTE_URL}")
print(f"Refspec: master -> master")
print()
try:
porcelain.push(
REPO_PATH,
REMOTE_URL,
refspecs=b"refs/heads/master:refs/heads/master",
username="oauth2",
password=token
)
print("PUSH ERFOLGREICH!")
except Exception as e:
print(f"FEHLER: {e}")
+126
View File
@@ -0,0 +1,126 @@
"""
Git Push v2 - mit korrekter Auth (oauth2:token als Basic Auth).
Behebt den 502-Fehler der durch falsche Auth-Methode verursacht wurde.
Usage:
python project-audit/scripts/git-push-v2.py "Commit-Nachricht"
"""
import os, sys
from pathlib import Path
from dulwich import porcelain
from dulwich.repo import Repo
REPO_PATH = Path(__file__).parent.parent # project-audit/
SECRETS_PATH = REPO_PATH / ".secrets"
REMOTE_URL = "https://git.tech.rz.db.de/AndreKnie/project-audit.git"
def load_token():
with open(SECRETS_PATH, "r", encoding="utf-8-sig") as f:
for line in f:
line = line.strip()
if line.startswith("GITLAB_TOKEN_AUDIT="):
return line.split("=", 1)[1].strip()
print("FEHLER: GITLAB_TOKEN_AUDIT nicht in .secrets!")
sys.exit(1)
def main():
message = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "Update pathOS Audit"
token = load_token()
print(f"=== Git Push v2: {message} ===")
print(f" Token: ...{token[-4:]}")
print(f" Auth: oauth2 (Basic Auth)")
# Stage + Commit falls noetig
from dulwich import porcelain as p
st = p.status(str(REPO_PATH))
needs_commit = st.unstaged or st.untracked
if needs_commit:
print(f" Unstaged: {len(st.unstaged)}, Untracked: {len(st.untracked)}")
# Stage all
patterns = []
gitignore = REPO_PATH / ".gitignore"
if gitignore.exists():
with open(gitignore) as f:
patterns = [l.strip().strip("/") for l in f if l.strip() and not l.startswith("#")]
staged = 0
for root, dirs, files in os.walk(REPO_PATH):
if ".git" in root:
continue
for fname in files:
full = Path(root) / fname
rel = full.relative_to(REPO_PATH)
rel_str = str(rel).replace("\\", "/")
if any(p in rel_str for p in patterns):
continue
try:
porcelain.add(str(REPO_PATH), paths=[str(rel)])
staged += 1
except Exception:
pass
print(f" {staged} Dateien gestaged")
try:
porcelain.commit(
str(REPO_PATH),
message=message.encode("utf-8"),
author=b"Andre Knie <andre.knie@deutschebahn.com>",
committer=b"Andre Knie <andre.knie@deutschebahn.com>"
)
print(f" Commit erstellt: {message}")
except Exception as e:
if "nothing to commit" not in str(e).lower():
print(f" Commit-Fehler: {e}")
else:
print(" Keine neuen Aenderungen, pushe bestehenden Commit.")
# Push mit oauth2 als Username (korrektes GitLab-Auth-Schema)
print(" Push zu origin (master)...")
try:
porcelain.push(
str(REPO_PATH),
REMOTE_URL,
refspecs=b"refs/heads/master:refs/heads/master",
username="oauth2",
password=token
)
print(" PUSH ERFOLGREICH!")
except Exception as e:
err = str(e)
print(f" Push-Fehler: {err}")
if "502" in err:
print(" 502 = Server-Problem oder Auth-Reject durch Proxy")
print(" Versuche Alternative: Push via GitLab API...")
push_via_api(token)
def push_via_api(token):
"""Fallback: Datei-Upload via GitLab Repository Files API."""
import urllib.request, json, ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
base = "https://git.tech.rz.db.de/api/v4/projects/AndreKnie%2Fproject-audit"
headers = {"PRIVATE-TOKEN": token, "Content-Type": "application/json"}
# Pruefe welche Dateien sich geaendert haben (vergleiche mit Remote)
print(" API-Fallback: Pruefe Remote-Status...")
url = f"{base}/repository/tree?per_page=100&ref=master"
req = urllib.request.Request(url)
req.add_header("PRIVATE-TOKEN", token)
try:
resp = urllib.request.urlopen(req, context=ctx, timeout=15)
print(f" Remote-Tree erreichbar. Aber API-Push ist komplex.")
print(" Empfehlung: git binary installieren oder spaeter erneut versuchen.")
except Exception as e2:
print(f" API-Fallback auch fehlgeschlagen: {e2}")
if __name__ == "__main__":
main()
+128
View File
@@ -0,0 +1,128 @@
"""
Git Push via dulwich (kein git binary noetig).
Liest Token aus .secrets, committed Aenderungen und pusht.
Usage:
python project-audit/scripts/git-push.py "Commit-Nachricht"
python project-audit/scripts/git-push.py (nutzt Standard-Nachricht)
"""
import os
import sys
from pathlib import Path
from dulwich import porcelain
from dulwich.repo import Repo
REPO_PATH = Path(__file__).parent.parent # project-audit/
SECRETS_PATH = REPO_PATH / ".secrets"
REMOTE_URL = "https://git.tech.rz.db.de/AndreKnie/project-audit.git"
def load_token():
"""Liest GITLAB_TOKEN_AUDIT aus .secrets Datei."""
if not SECRETS_PATH.exists():
print(f"FEHLER: {SECRETS_PATH} nicht gefunden!")
print("Erstelle die Datei mit: GITLAB_TOKEN_AUDIT=dein_token")
sys.exit(1)
with open(SECRETS_PATH, "r", encoding="utf-8-sig") as f:
for line in f:
line = line.strip()
if line.startswith("GITLAB_TOKEN_AUDIT="):
val = line.split("=", 1)[1].strip()
if val and val != "HIER_TOKEN_EINTRAGEN":
return val
print("FEHLER: GITLAB_TOKEN_AUDIT nicht in .secrets gesetzt!")
print("Trage deinen Token ein: GITLAB_TOKEN_AUDIT=glpat-xxxxx")
sys.exit(1)
def get_ignore_patterns():
"""Liest .gitignore Patterns."""
patterns = []
gitignore = REPO_PATH / ".gitignore"
if gitignore.exists():
with open(gitignore, "r") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#"):
patterns.append(line.strip("/"))
return patterns
def should_ignore(rel_path, patterns):
"""Prueft ob Datei ignoriert werden soll."""
rel_str = str(rel_path).replace("\\", "/")
for p in patterns:
if p in rel_str:
return True
return False
def main():
message = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "Update pathOS Audit"
print(f"=== Git Push: {message} ===")
print(f" Repo: {REPO_PATH}")
print(f" Remote: {REMOTE_URL}")
# Token laden
token = load_token()
print(f" Token: ...{token[-4:]}")
# Repo oeffnen
if not (REPO_PATH / ".git").exists():
print(" Kein Repo gefunden, initialisiere...")
Repo.init(str(REPO_PATH))
# Alle Dateien stagen (neue + geaenderte)
print(" Stage Aenderungen...")
patterns = get_ignore_patterns()
staged = 0
for root, dirs, files in os.walk(REPO_PATH):
if ".git" in root:
continue
for fname in files:
full_path = Path(root) / fname
rel_path = full_path.relative_to(REPO_PATH)
if should_ignore(rel_path, patterns):
continue
try:
porcelain.add(str(REPO_PATH), paths=[str(rel_path)])
staged += 1
except Exception:
pass
print(f" {staged} Dateien gestaged")
# Commit
print(f" Commit: {message}")
try:
porcelain.commit(
str(REPO_PATH),
message=message.encode("utf-8"),
author=b"Andre Knie <andre.knie@deutschebahn.com>",
committer=b"Andre Knie <andre.knie@deutschebahn.com>"
)
except Exception as e:
if "nothing to commit" in str(e).lower():
print(" Keine Aenderungen zum Committen.")
return
print(f" Commit-Fehler: {e}")
# Push
print(" Push zu origin...")
try:
porcelain.push(
str(REPO_PATH),
REMOTE_URL,
refspecs=b"refs/heads/master:refs/heads/master",
username="AndreKnie",
password=token
)
print(" PUSH ERFOLGREICH!" )
except Exception as e:
print(f" Push-Fehler: {e}")
print(" Tipp: Pruefen ob das Remote-Repo existiert und der Token Schreibrechte hat.")
if __name__ == "__main__":
main()
@@ -0,0 +1,72 @@
"""Test: Git Push Auth-Strategien."""
import urllib.request, ssl, base64
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
token = secrets.get("GITLAB_TOKEN_AUDIT", "")
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
base_url = "https://git.tech.rz.db.de/AndreKnie/project-audit.git"
# Test 1: Basic Auth mit oauth2:token (GitLab-Standard fuer PAT ueber HTTP)
print("=== Test 1: Basic Auth (oauth2:token) ===")
url1 = base_url + "/info/refs?service=git-receive-pack"
creds = base64.b64encode(f"oauth2:{token}".encode()).decode()
req1 = urllib.request.Request(url1)
req1.add_header("Authorization", "Basic " + creds)
try:
resp1 = urllib.request.urlopen(req1, context=ctx, timeout=15)
print("Status:", resp1.status)
body = resp1.read(300)
print("OK! Endpunkt erreichbar.")
print("Body:", body[:200])
except Exception as e:
print("FEHLER:", e)
print()
# Test 2: Basic Auth mit username:token
print("=== Test 2: Basic Auth (AndreKnie:token) ===")
creds2 = base64.b64encode(f"AndreKnie:{token}".encode()).decode()
req2 = urllib.request.Request(url1)
req2.add_header("Authorization", "Basic " + creds2)
try:
resp2 = urllib.request.urlopen(req2, context=ctx, timeout=15)
print("Status:", resp2.status)
print("OK! Endpunkt erreichbar.")
except Exception as e:
print("FEHLER:", e)
print()
# Test 3: Token in URL (https://oauth2:token@host/...)
print("=== Test 3: Token in URL ===")
url3 = f"https://oauth2:{token}@git.tech.rz.db.de/AndreKnie/project-audit.git/info/refs?service=git-receive-pack"
req3 = urllib.request.Request(url3)
try:
resp3 = urllib.request.urlopen(req3, context=ctx, timeout=15)
print("Status:", resp3.status)
print("OK! Endpunkt erreichbar.")
except Exception as e:
print("FEHLER:", e)
print()
# Test 4: upload-pack mit Basic Auth (Lese-Endpunkt zum Vergleich)
print("=== Test 4: upload-pack mit Basic Auth (Vergleich) ===")
url4 = base_url + "/info/refs?service=git-upload-pack"
req4 = urllib.request.Request(url4)
req4.add_header("Authorization", "Basic " + creds)
try:
resp4 = urllib.request.urlopen(req4, context=ctx, timeout=15)
print("Status:", resp4.status)
print("OK! Lese-Endpunkt erreichbar.")
except Exception as e:
print("FEHLER:", e)
@@ -0,0 +1,182 @@
"""
GitLab Branch-Analyse: Aktive Branches der Kern-Services.
Nutzt Projekt-IDs direkt (zuverlaessiger als Pfad-Encoding).
"""
import urllib.request, json, ssl
from datetime import datetime, timezone
from pathlib import Path
from collections import defaultdict
GITLAB_URL = "https://git.tech.rz.db.de"
API = f"{GITLAB_URL}/api/v4"
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
TOKEN = secrets.get("GITLAB_TOKEN_BESTELLSYSTEM", "")
if not TOKEN:
print("FEHLER: GITLAB_TOKEN_BESTELLSYSTEM nicht in .secrets!")
exit(1)
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
def api_get(path):
url = f"{API}{path}"
req = urllib.request.Request(url)
req.add_header("PRIVATE-TOKEN", TOKEN)
try:
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
return json.loads(resp.read().decode("utf-8"))
except Exception as e:
return None
# Kern-Services mit IDs (aus gitlab-find-projects.py)
CORE_PROJECTS = {
"portal-middleware": 56664,
"portal-ui": 51502,
"steuerung-vertrieb": 54688,
"auftrags-verwaltung-trasse": 63436,
"common-interface": 52825,
"stammdaten-bereitstellung": 150168,
"tadef-connector": 352680,
"archivierungsservice": 294401,
"ifp-connector": 256070,
"kundendaten-bereitstellung": 109956,
"vertragsdaten-verteiler": 352409,
"rabattnummern-bereitstellung": 216978,
"taftap-tdm-konverter": 256243,
}
print("=== GitLab Branch-Analyse ===")
print(f"Datum: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print(f"Projekte: {len(CORE_PROJECTS)}")
print()
now = datetime.now(timezone.utc)
all_branches = []
api_calls = 0
for proj_name, proj_id in CORE_PROJECTS.items():
# Branches laden (paginiert)
branches = []
page = 1
while True:
data = api_get(f"/projects/{proj_id}/repository/branches?per_page=100&page={page}")
api_calls += 1
if not data:
break
branches.extend(data)
if len(data) < 100:
break
page += 1
feature_branches = []
for b in branches:
name = b["name"]
if name in ("master", "main", "develop"):
continue
commit = b.get("commit", {})
committed_date = commit.get("committed_date", "")
author = commit.get("author_name", "?")
if committed_date:
try:
dt = datetime.fromisoformat(committed_date.replace("Z", "+00:00"))
age_days = (now - dt).days
except:
age_days = -1
else:
age_days = -1
feature_branches.append({
"project": proj_name,
"branch": name,
"author": author,
"age_days": age_days,
"date": committed_date[:10] if committed_date else "?",
})
all_branches.extend(feature_branches)
active = [b for b in feature_branches if 0 <= b["age_days"] <= 30]
stale = [b for b in feature_branches if b["age_days"] > 30]
print(f" {proj_name:<35} {len(feature_branches):>3} Branches ({len(active)} aktiv, {len(stale)} stale)")
print(f"\nAPI-Calls: {api_calls}")
print(f"Branches gesamt: {len(all_branches)}")
# Aktive Branches (letzte 30 Tage)
active_branches = [b for b in all_branches if 0 <= b["age_days"] <= 30]
active_branches.sort(key=lambda x: x["age_days"])
print(f"\n--- Aktive Branches (letzte 30 Tage): {len(active_branches)} ---")
print(f"{'Projekt':<30} {'Branch':<60} {'Autor':<25} {'Tage'}")
print("-" * 125)
for b in active_branches[:50]:
branch_short = b["branch"][:59]
print(f"{b['project']:<30} {branch_short:<60} {b['author']:<25} {b['age_days']}d")
# Stale Branches (>60 Tage)
stale_branches = [b for b in all_branches if b["age_days"] > 60]
stale_branches.sort(key=lambda x: x["age_days"], reverse=True)
print(f"\n--- Stale Branches (>60 Tage): {len(stale_branches)} ---")
if stale_branches:
print(f"{'Projekt':<30} {'Branch':<60} {'Tage'}")
print("-" * 95)
for b in stale_branches[:20]:
branch_short = b["branch"][:59]
print(f"{b['project']:<30} {branch_short:<60} {b['age_days']}d")
# Analyse: Wer arbeitet an was?
print(f"\n--- Aktivitaet pro Autor (letzte 30 Tage) ---")
authors = defaultdict(list)
for b in active_branches:
authors[b["author"]].append(b)
for author, branches in sorted(authors.items(), key=lambda x: -len(x[1]))[:20]:
projects = set(b["project"] for b in branches)
print(f" {author:<30} {len(branches):>2} Branches in: {', '.join(sorted(projects))}")
# Spring Boot 4 Branches
sb4_branches = [b for b in all_branches if "spring" in b["branch"].lower() or "boot-4" in b["branch"].lower() or "springboot" in b["branch"].lower()]
print(f"\n--- Spring Boot 4 Branches: {len(sb4_branches)} ---")
for b in sorted(sb4_branches, key=lambda x: x["age_days"]):
status = "AKTIV" if b["age_days"] <= 30 else f"STALE ({b['age_days']}d)"
print(f" {b['project']:<30} {b['branch']:<55} {status}")
# Renovate Branches
renovate_branches = [b for b in all_branches if "renovate" in b["branch"].lower()]
renovate_active = [b for b in renovate_branches if b["age_days"] <= 7]
renovate_stale = [b for b in renovate_branches if b["age_days"] > 30]
print(f"\n--- Renovate Branches: {len(renovate_branches)} gesamt ---")
print(f" Frisch (<=7d): {len(renovate_active)}")
print(f" Stale (>30d): {len(renovate_stale)}")
# Ticket-Extraktion aus Branch-Namen
print(f"\n--- Jira-Tickets in aktiven Branches ---")
import re
tickets = defaultdict(list)
for b in active_branches:
matches = re.findall(r'(O2C[A-Z]+-\d+|TTTI-\d+)', b["branch"].upper())
for ticket in matches:
tickets[ticket].append(b)
if tickets:
for ticket, branches in sorted(tickets.items()):
projs = set(b["project"] for b in branches)
print(f" {ticket:<15} in {', '.join(sorted(projs))}")
# CSV Export
csv_path = Path("project-audit/data/gitlab-branches.csv")
with open(csv_path, "w", encoding="utf-8") as f:
f.write('"Project";"Branch";"Author";"LastCommit";"AgeDays"\n')
for b in sorted(all_branches, key=lambda x: (x["project"], x["age_days"])):
f.write(f'"{b["project"]}";"{b["branch"]}";"{b["author"]}";"{b["date"]}";"{b["age_days"]}"\n')
print(f"\nCSV exportiert: {csv_path}")
@@ -0,0 +1,55 @@
"""Prueft ob Shared Libraries eigene Repos mit Tests haben."""
import urllib.request, json, ssl
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
TOKEN = secrets.get("GITLAB_TOKEN_BESTELLSYSTEM", "")
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
def api_get(url):
req = urllib.request.Request(url)
req.add_header("PRIVATE-TOKEN", TOKEN)
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
return json.loads(resp.read().decode("utf-8"))
# Alle Projekte in libraries/ Subgruppe
print("=== Libraries-Subgruppe ===")
url = "https://git.tech.rz.db.de/api/v4/groups/bestellsystem1/projects?per_page=100&include_subgroups=true"
all_projects = api_get(url)
libs = [p for p in all_projects if "/libraries/" in p["path_with_namespace"]]
print(f"Projekte in libraries/: {len(libs)}")
for p in libs:
print(f" {p['path_with_namespace']:<60} ID={p['id']} Aktiv={p.get('last_activity_at','')[:10]}")
# Fuer jede Library: CI-Pipeline pruefen (hat sie .gitlab-ci.yml?)
print()
print("=== CI-Pipeline und Tests pruefen ===")
for p in libs:
pid = p["id"]
name = p["path_with_namespace"].split("/")[-1]
# Pruefen ob .gitlab-ci.yml existiert
try:
ci_url = f"https://git.tech.rz.db.de/api/v4/projects/{pid}/repository/files/.gitlab-ci.yml?ref=master"
ci_data = api_get(ci_url)
has_ci = True
except:
has_ci = False
# Pruefen ob src/test existiert
try:
test_url = f"https://git.tech.rz.db.de/api/v4/projects/{pid}/repository/tree?path=src/test&ref=master"
test_data = api_get(test_url)
has_tests = len(test_data) > 0 if test_data else False
except:
has_tests = False
ci_str = "CI" if has_ci else "kein CI"
test_str = "Tests vorhanden" if has_tests else "KEINE Tests"
print(f" {name:<40} {ci_str:<8} {test_str}")
@@ -0,0 +1,268 @@
<#
.SYNOPSIS
GitLab Projekt-Inventar fuer pathOS / Bestellsystem
.DESCRIPTION
Zieht alle Projekte aus der GitLab-Gruppe und erstellt eine strukturierte Uebersicht.
Kompatibel mit PowerShell 5.1 keine zusaetzlichen Installationen noetig.
#>
$GitLabUrl = "https://git.tech.rz.db.de"
$GroupPath = "bestellsystem1"
$ApiBase = "$GitLabUrl/api/v4"
$PerPage = 100
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$OutputDir = Join-Path (Split-Path -Parent $ScriptDir) "data\gitlab-inventory"
# SSL-Handling (auskommentieren falls selbstsigniertes Zertifikat)
# [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
# TLS 1.2 erzwingen
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Token
$Token = Read-Host -Prompt "GitLab Personal Access Token (read_api)"
$Headers = @{ "PRIVATE-TOKEN" = $Token }
function Safe-String($val) {
if ($null -eq $val) { return "" }
return [string]$val
}
function Invoke-GitLabApi {
param([string]$Endpoint, [hashtable]$Params = @{})
$Params["per_page"] = $PerPage
$AllResults = @()
$Page = 1
do {
$Params["page"] = $Page
$QS = ($Params.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join "&"
$Uri = "$ApiBase$Endpoint`?$QS"
try {
$Resp = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
$Data = $Resp.Content | ConvertFrom-Json
if ($null -eq $Data) { break }
if ($Data -is [array]) {
if ($Data.Count -eq 0) { break }
$AllResults += $Data
} else {
return $Data
}
$TP = 1
if ($Resp.Headers["X-Total-Pages"]) { $TP = [int]$Resp.Headers["X-Total-Pages"] }
Write-Host " Seite $Page/$TP ($($Data.Count) Eintraege)" -ForegroundColor DarkGray
$Page++
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
throw
}
} while ($Page -le $TP)
return $AllResults
}
function Get-ProjectLanguages($ProjectId) {
try {
$Uri = "$ApiBase/projects/$ProjectId/languages"
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
return ($R.Content | ConvertFrom-Json)
} catch { return $null }
}
function Get-ProjectReadme($ProjectId) {
foreach ($Branch in @("main", "master", "develop")) {
try {
$Uri = "$ApiBase/projects/$ProjectId/repository/files/README.md/raw?ref=$Branch"
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
$C = $R.Content
if ($C.Length -gt 3000) { $C = $C.Substring(0, 3000) + "`n`n... (gekuerzt)" }
return $C
} catch { continue }
}
return $null
}
function Get-RepoTree($ProjectId) {
foreach ($Branch in @("main", "master", "develop")) {
try {
$Uri = "$ApiBase/projects/$ProjectId/repository/tree?ref=$Branch&per_page=100"
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
return ($R.Content | ConvertFrom-Json)
} catch { continue }
}
return @()
}
function Get-ProjectTags($Project, $Languages, $Tree) {
$Tags = @()
$Name = (Safe-String $Project.name).ToLower()
$Desc = (Safe-String $Project.description).ToLower()
$Files = @($Tree | ForEach-Object { $_.name.ToLower() })
if ($Languages) {
$LangProps = $Languages | Get-Member -MemberType NoteProperty
foreach ($lp in $LangProps) {
$lang = $lp.Name.ToLower()
if ($lang -eq "java") { $Tags += "java" }
if ($lang -eq "kotlin") { $Tags += "kotlin" }
if ($lang -match "typescript|javascript") { $Tags += "frontend" }
if ($lang -eq "python") { $Tags += "python" }
}
}
if ("pom.xml" -in $Files) { $Tags += "maven" }
if ("build.gradle" -in $Files -or "build.gradle.kts" -in $Files) { $Tags += "gradle" }
if ("package.json" -in $Files) { $Tags += "npm" }
if ("Dockerfile" -in $Files -or "dockerfile" -in $Files) { $Tags += "docker" }
if ($Name -match "lib|library|common|shared|util" -or $Desc -match "lib|library|common|shared|util") { $Tags += "library" }
if ($Name -match "service|api|backend" -or $Desc -match "service|api|backend") { $Tags += "service" }
if ($Name -match "ui|frontend|web|app" -or $Desc -match "ui|frontend|web|app") { $Tags += "frontend-app" }
if ($Name -match "test|e2e|integration" -or $Desc -match "test|e2e|integration") { $Tags += "testing" }
if ($Name -match "config|infra|deploy|pipeline|ci" -or $Desc -match "config|infra|deploy|pipeline|ci") { $Tags += "infrastructure" }
if ($Project.last_activity_at) {
try {
$Last = [DateTime]::Parse($Project.last_activity_at)
$Days = ((Get-Date) - $Last).Days
if ($Days -gt 365) { $Tags += "inactive-1y+" }
elseif ($Days -gt 180) { $Tags += "inactive-6m+" }
} catch {}
}
if ($Project.archived) { $Tags += "archived" }
return ($Tags | Select-Object -Unique)
}
# === MAIN ===
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " pathOS / Bestellsystem - GitLab Projekt-Inventar" -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " GitLab: $GitLabUrl"
Write-Host " Gruppe: $GroupPath"
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
New-Item -ItemType Directory -Path "$OutputDir\readmes" -Force | Out-Null
try {
Write-Host "`n>> Lade Gruppeninfo..." -ForegroundColor Yellow
$EncodedGroup = [Uri]::EscapeDataString($GroupPath)
$GroupInfo = Invoke-GitLabApi -Endpoint "/groups/$EncodedGroup"
Write-Host " Gruppe: $($GroupInfo.name) (ID: $($GroupInfo.id))" -ForegroundColor Green
Write-Host "`n>> Lade Untergruppen..." -ForegroundColor Yellow
$Subgroups = @(Invoke-GitLabApi -Endpoint "/groups/$EncodedGroup/subgroups" -Params @{ all_available = "true" })
Write-Host " $($Subgroups.Count) Untergruppen" -ForegroundColor Green
Write-Host "`n>> Lade alle Projekte..." -ForegroundColor Yellow
$Projects = @(Invoke-GitLabApi -Endpoint "/groups/$EncodedGroup/projects" -Params @{
include_subgroups = "true"; with_shared = "false"; order_by = "name"; sort = "asc"
})
Write-Host " $($Projects.Count) Projekte" -ForegroundColor Green
Write-Host "`n>> Reichere Projekte an..." -ForegroundColor Yellow
$Enriched = @()
$Total = $Projects.Count
$i = 0
foreach ($P in $Projects) {
$i++
Write-Host " [$i/$Total] $($P.name)..." -NoNewline
$Langs = Get-ProjectLanguages $P.id
$Readme = Get-ProjectReadme $P.id
$Tree = @(Get-RepoTree $P.id)
$Tags = @(Get-ProjectTags $P $Langs $Tree)
$Obj = [PSCustomObject]@{
id = $P.id
name = $P.name
path = $P.path_with_namespace
description = Safe-String $P.description
branch = $P.default_branch
archived = $P.archived
created = $P.created_at
last_active = $P.last_activity_at
namespace = $P.namespace.full_path
web_url = $P.web_url
languages = $Langs
tags = $Tags
files = @($Tree | ForEach-Object { $_.name })
readme = $Readme
}
$Enriched += $Obj
if ($Readme) {
$SafeName = $P.path_with_namespace -replace "/", "__"
"# $($P.name)`n`n> Quelle: $($P.web_url)`n`n$Readme" | Out-File "$OutputDir\readmes\$SafeName.md" -Encoding UTF8
}
Write-Host " OK" -ForegroundColor Green
}
# JSON
$Enriched | ConvertTo-Json -Depth 10 | Out-File "$OutputDir\projects-full.json" -Encoding UTF8
Write-Host "`n JSON gespeichert" -ForegroundColor DarkGray
# CSV
$CsvRows = $Enriched | ForEach-Object {
$LS = ""
if ($_.languages) {
$LP = $_.languages | Get-Member -MemberType NoteProperty
$LS = ($LP | ForEach-Object { $_.Name }) -join ", "
}
[PSCustomObject]@{
Name=$_.name; Pfad=$_.path; Beschreibung=$_.description;
Sprachen=$LS; Tags=($_.tags -join ", "); LetzteAktivitaet=$_.last_active;
Archiviert=$_.archived; Branch=$_.branch; Namespace=$_.namespace
}
}
$CsvRows | Export-Csv "$OutputDir\projects-overview.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
Write-Host " CSV gespeichert" -ForegroundColor DarkGray
# Markdown
$Md = @("# GitLab Projektuebersicht - $(Safe-String $GroupInfo.name)", "",
"> Generiert: $(Get-Date -Format 'yyyy-MM-dd HH:mm')",
"> Projekte: $($Enriched.Count)", "")
if ($Subgroups.Count -gt 0) {
$Md += "## Untergruppen"; $Md += ""
foreach ($sg in $Subgroups) { $Md += "- **$($sg.name)** (``$($sg.full_path)``)" }
$Md += ""
}
$ByNs = $Enriched | Group-Object -Property namespace
foreach ($G in ($ByNs | Sort-Object Name)) {
$Md += "## $($G.Name)"; $Md += ""
$Md += "| Projekt | Beschreibung | Sprachen | Tags | Aktiv |"
$Md += "|---------|-------------|----------|------|-------|"
foreach ($p in $G.Group) {
$LS = ""; if ($p.languages) { $LP = $p.languages | Get-Member -MemberType NoteProperty; $LS = ($LP | ForEach-Object { $_.Name }) -join ", " }
$TS = ($p.tags -join ", ")
$DS = if ($p.description.Length -gt 60) { $p.description.Substring(0,60) + "..." } else { $p.description }
if (-not $DS) { $DS = "-" }
$LA = if ($p.last_active) { $p.last_active.Substring(0,10) } else { "-" }
$Md += "| $($p.name) | $DS | $LS | $TS | $LA |"
}
$Md += ""
}
$Md -join "`n" | Out-File "$OutputDir\projects-overview.md" -Encoding UTF8
Write-Host " Markdown gespeichert" -ForegroundColor DarkGray
# Statistiken
$St = @("# Projektstatistiken", "", "- Projekte: $($Enriched.Count)", "- Untergruppen: $($Subgroups.Count)",
"- Archiviert: $(($Enriched | Where-Object { $_.archived }).Count)", "", "## Sprachen")
$LC = @{}; foreach ($p in $Enriched) { if ($p.languages) { $p.languages | Get-Member -MemberType NoteProperty | ForEach-Object { $LC[$_.Name] = ($LC[$_.Name]) + 1 } } }
foreach ($e in ($LC.GetEnumerator() | Sort-Object Value -Descending)) { $St += "- $($e.Key): $($e.Value)" }
$St += ""; $St += "## Tags"
$TC = @{}; foreach ($p in $Enriched) { foreach ($t in $p.tags) { $TC[$t] = ($TC[$t]) + 1 } }
foreach ($e in ($TC.GetEnumerator() | Sort-Object Value -Descending)) { $St += "- $($e.Key): $($e.Value)" }
$St -join "`n" | Out-File "$OutputDir\statistics.md" -Encoding UTF8
Write-Host " Statistiken gespeichert" -ForegroundColor DarkGray
Write-Host "`n============================================================" -ForegroundColor Green
Write-Host " FERTIG! $($Enriched.Count) Projekte inventarisiert." -ForegroundColor Green
Write-Host " Ergebnisse: $OutputDir" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
} catch {
Write-Host "`nFEHLER: $($_.Exception.Message)" -ForegroundColor Red
if ($_.Exception.Message -match "401") { Write-Host " Token pruefen (read_api Scope)." -ForegroundColor Yellow }
if ($_.Exception.Message -match "404") { Write-Host " Gruppe '$GroupPath' nicht gefunden." -ForegroundColor Yellow }
if ($_.Exception.Message -match "resolve|Remotename") { Write-Host " VPN aktiv?" -ForegroundColor Yellow }
exit 1
}
@@ -0,0 +1,407 @@
#!/usr/bin/env python3
"""
GitLab Project Inventory Script für pathOS / Bestellsystem
===========================================================
Zieht alle Projekte aus der GitLab-Gruppe und erstellt eine strukturierte Übersicht.
Voraussetzungen:
- Python 3.8+
- pip install requests
Verwendung:
python gitlab-inventory.py
Du wirst nach deinem Personal Access Token gefragt (read_api Scope reicht).
"""
import requests
import json
import csv
import os
import sys
from datetime import datetime
from getpass import getpass
from pathlib import Path
# === Konfiguration ===
GITLAB_URL = "https://git.tech.rz.db.de"
GROUP_PATH = "bestellsystem1"
API_BASE = f"{GITLAB_URL}/api/v4"
OUTPUT_DIR = Path(__file__).parent.parent / "data" / "gitlab-inventory"
PER_PAGE = 100
def get_token():
"""Token aus Umgebungsvariable oder interaktiver Eingabe."""
token = os.environ.get("GITLAB_TOKEN")
if not token:
token = getpass("GitLab Personal Access Token (read_api): ")
return token
def api_get(session, endpoint, params=None):
"""Paginierte API-Abfrage mit automatischem Durchblättern."""
if params is None:
params = {}
params["per_page"] = PER_PAGE
all_results = []
page = 1
while True:
params["page"] = page
resp = session.get(f"{API_BASE}{endpoint}", params=params)
resp.raise_for_status()
data = resp.json()
if not data:
break
all_results.extend(data)
total_pages = int(resp.headers.get("X-Total-Pages", 1))
print(f" Seite {page}/{total_pages} geladen ({len(data)} Einträge)")
if page >= total_pages:
break
page += 1
return all_results
def fetch_group_info(session):
"""Gruppeninformationen abrufen."""
print(f"\n📁 Lade Gruppeninfo für '{GROUP_PATH}'...")
resp = session.get(f"{API_BASE}/groups/{requests.utils.quote(GROUP_PATH, safe='')}")
resp.raise_for_status()
return resp.json()
def fetch_all_projects(session):
"""Alle Projekte der Gruppe inkl. Untergruppen."""
print("\n📦 Lade alle Projekte (inkl. Untergruppen)...")
projects = api_get(
session,
f"/groups/{requests.utils.quote(GROUP_PATH, safe='')}/projects",
params={
"include_subgroups": "true",
"with_shared": "false",
"order_by": "name",
"sort": "asc",
},
)
print(f"{len(projects)} Projekte gefunden")
return projects
def fetch_subgroups(session):
"""Alle Untergruppen der Hauptgruppe."""
print("\n📂 Lade Untergruppen...")
subgroups = api_get(
session,
f"/groups/{requests.utils.quote(GROUP_PATH, safe='')}/subgroups",
params={"all_available": "true"},
)
print(f"{len(subgroups)} Untergruppen gefunden")
return subgroups
def fetch_project_languages(session, project_id):
"""Programmiersprachen eines Projekts."""
try:
resp = session.get(f"{API_BASE}/projects/{project_id}/languages")
resp.raise_for_status()
return resp.json()
except Exception:
return {}
def fetch_project_readme(session, project_id):
"""README.md eines Projekts (falls vorhanden)."""
try:
resp = session.get(
f"{API_BASE}/projects/{project_id}/repository/files/README.md/raw",
params={"ref": "main"},
)
if resp.status_code == 404:
resp = session.get(
f"{API_BASE}/projects/{project_id}/repository/files/README.md/raw",
params={"ref": "master"},
)
if resp.status_code == 200:
return resp.text[:3000] # Erste 3000 Zeichen
except Exception:
pass
return None
def fetch_repo_tree(session, project_id, path="", ref="main"):
"""Top-Level Verzeichnisstruktur eines Repos."""
try:
resp = session.get(
f"{API_BASE}/projects/{project_id}/repository/tree",
params={"ref": ref, "path": path, "per_page": 100},
)
if resp.status_code == 404 and ref == "main":
return fetch_repo_tree(session, project_id, path, "master")
if resp.status_code == 200:
return resp.json()
except Exception:
pass
return []
def enrich_project(session, project, index, total):
"""Projekt mit zusätzlichen Details anreichern."""
pid = project["id"]
name = project["name"]
print(f" [{index}/{total}] {name}...", end=" ", flush=True)
# Sprachen
languages = fetch_project_languages(session, pid)
project["_languages"] = languages
# README (erste 3000 Zeichen)
readme = fetch_project_readme(session, pid)
project["_readme_excerpt"] = readme
# Top-Level Dateistruktur
tree = fetch_repo_tree(session, pid)
project["_top_level_files"] = [
{"name": f["name"], "type": f["type"]} for f in tree
]
print("")
return project
def classify_project(project):
"""Projekt grob klassifizieren basierend auf verfügbaren Daten."""
name = project.get("name", "").lower()
desc = (project.get("description") or "").lower()
topics = [t.lower() for t in project.get("topics", [])]
files = [f["name"].lower() for f in project.get("_top_level_files", [])]
languages = project.get("_languages", {})
tags = []
# Sprach-Tags
if "Java" in languages:
tags.append("java")
if "Kotlin" in languages:
tags.append("kotlin")
if "TypeScript" in languages or "JavaScript" in languages:
tags.append("frontend")
if "Python" in languages:
tags.append("python")
# Build-System
if "pom.xml" in files:
tags.append("maven")
if "build.gradle" in files or "build.gradle.kts" in files:
tags.append("gradle")
if "package.json" in files:
tags.append("npm")
if "Dockerfile" in files or "dockerfile" in files:
tags.append("docker")
if any("helm" in f for f in files) or "Chart.yaml" in files:
tags.append("helm")
# Typ-Erkennung
if any(kw in name or kw in desc for kw in ["lib", "library", "common", "shared", "util"]):
tags.append("library")
if any(kw in name or kw in desc for kw in ["service", "api", "backend"]):
tags.append("service")
if any(kw in name or kw in desc for kw in ["ui", "frontend", "web", "app"]):
tags.append("frontend-app")
if any(kw in name or kw in desc for kw in ["test", "e2e", "integration"]):
tags.append("testing")
if any(kw in name or kw in desc for kw in ["config", "infra", "deploy", "pipeline", "ci"]):
tags.append("infrastructure")
if any(kw in name or kw in desc for kw in ["doc", "wiki", "readme"]):
tags.append("documentation")
# Aktivitätsstatus
last_activity = project.get("last_activity_at", "")
if last_activity:
try:
last = datetime.fromisoformat(last_activity.replace("Z", "+00:00"))
days_ago = (datetime.now(last.tzinfo) - last).days
if days_ago > 365:
tags.append("inactive-1y+")
elif days_ago > 180:
tags.append("inactive-6m+")
except Exception:
pass
if project.get("archived"):
tags.append("archived")
return tags
def save_outputs(projects, subgroups, group_info):
"""Alle Ergebnisse speichern."""
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# 1. Vollständige JSON-Daten
json_path = OUTPUT_DIR / "projects-full.json"
with open(json_path, "w", encoding="utf-8") as f:
json.dump(projects, f, indent=2, ensure_ascii=False, default=str)
print(f"\n💾 Vollständige Daten: {json_path}")
# 2. Übersichts-CSV
csv_path = OUTPUT_DIR / "projects-overview.csv"
with open(csv_path, "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f, delimiter=";")
writer.writerow([
"Name", "Pfad", "Beschreibung", "Sprachen", "Tags",
"Letzte Aktivität", "Erstellt", "Archiviert",
"Default Branch", "Namespace"
])
for p in projects:
langs = ", ".join(f"{k} ({v:.0f}%)" for k, v in p.get("_languages", {}).items())
tags = ", ".join(p.get("_tags", []))
writer.writerow([
p.get("name", ""),
p.get("path_with_namespace", ""),
(p.get("description") or "")[:200],
langs,
tags,
p.get("last_activity_at", ""),
p.get("created_at", ""),
p.get("archived", False),
p.get("default_branch", ""),
p.get("namespace", {}).get("full_path", ""),
])
print(f"📊 Übersichts-CSV: {csv_path}")
# 3. Markdown-Übersicht
md_path = OUTPUT_DIR / "projects-overview.md"
with open(md_path, "w", encoding="utf-8") as f:
f.write(f"# GitLab Projektübersicht — {group_info.get('name', GROUP_PATH)}\n\n")
f.write(f"> Generiert am {datetime.now().strftime('%Y-%m-%d %H:%M')}\n")
f.write(f"> Quelle: {GITLAB_URL}/{GROUP_PATH}\n")
f.write(f"> Anzahl Projekte: {len(projects)}\n\n")
# Untergruppen
if subgroups:
f.write("## Untergruppen\n\n")
for sg in subgroups:
f.write(f"- **{sg['name']}** (`{sg['full_path']}`) — {sg.get('description', 'Keine Beschreibung')}\n")
f.write("\n")
# Projekte nach Namespace gruppiert
by_namespace = {}
for p in projects:
ns = p.get("namespace", {}).get("full_path", "root")
by_namespace.setdefault(ns, []).append(p)
for ns in sorted(by_namespace.keys()):
f.write(f"## {ns}\n\n")
f.write("| Projekt | Beschreibung | Sprachen | Tags | Letzte Aktivität |\n")
f.write("|---------|-------------|----------|------|------------------|\n")
for p in by_namespace[ns]:
langs = ", ".join(p.get("_languages", {}).keys())
tags = ", ".join(p.get("_tags", []))
desc = (p.get("description") or "")[:80]
last = (p.get("last_activity_at") or "")[:10]
f.write(f"| {p['name']} | {desc} | {langs} | {tags} | {last} |\n")
f.write("\n")
print(f"📝 Markdown-Übersicht: {md_path}")
# 4. README-Sammlung
readme_dir = OUTPUT_DIR / "readmes"
readme_dir.mkdir(exist_ok=True)
count = 0
for p in projects:
if p.get("_readme_excerpt"):
safe_name = p["path_with_namespace"].replace("/", "__")
with open(readme_dir / f"{safe_name}.md", "w", encoding="utf-8") as f:
f.write(f"# {p['name']}\n\n")
f.write(f"> Quelle: {GITLAB_URL}/{p['path_with_namespace']}\n\n")
f.write(p["_readme_excerpt"])
count += 1
print(f"📖 READMEs gespeichert: {count} Dateien in {readme_dir}")
# 5. Statistik-Zusammenfassung
stats_path = OUTPUT_DIR / "statistics.md"
with open(stats_path, "w", encoding="utf-8") as f:
f.write("# Projektstatistiken\n\n")
f.write(f"- **Gesamtanzahl Projekte**: {len(projects)}\n")
f.write(f"- **Untergruppen**: {len(subgroups)}\n")
f.write(f"- **Archiviert**: {sum(1 for p in projects if p.get('archived'))}\n")
# Sprachen-Verteilung
lang_count = {}
for p in projects:
for lang in p.get("_languages", {}):
lang_count[lang] = lang_count.get(lang, 0) + 1
f.write("\n## Sprachen-Verteilung\n\n")
for lang, count in sorted(lang_count.items(), key=lambda x: -x[1]):
f.write(f"- {lang}: {count} Projekte\n")
# Tag-Verteilung
tag_count = {}
for p in projects:
for tag in p.get("_tags", []):
tag_count[tag] = tag_count.get(tag, 0) + 1
f.write("\n## Tag-Verteilung\n\n")
for tag, count in sorted(tag_count.items(), key=lambda x: -x[1]):
f.write(f"- {tag}: {count} Projekte\n")
print(f"📈 Statistiken: {stats_path}")
def main():
print("=" * 60)
print(" pathOS / Bestellsystem — GitLab Projekt-Inventar")
print("=" * 60)
print(f"\n GitLab: {GITLAB_URL}")
print(f" Gruppe: {GROUP_PATH}")
token = get_token()
session = requests.Session()
session.headers["PRIVATE-TOKEN"] = token
session.verify = True # Auf False setzen falls selbstsigniertes Zertifikat
try:
# Gruppeninfo
group_info = fetch_group_info(session)
print(f" ✓ Gruppe gefunden: {group_info.get('name')} (ID: {group_info.get('id')})")
# Untergruppen
subgroups = fetch_subgroups(session)
# Alle Projekte
projects = fetch_all_projects(session)
# Projekte anreichern
print(f"\n🔍 Reichere {len(projects)} Projekte mit Details an...")
for i, project in enumerate(projects, 1):
enrich_project(session, project, i, len(projects))
project["_tags"] = classify_project(project)
# Ergebnisse speichern
save_outputs(projects, subgroups, group_info)
print("\n✅ Fertig! Alle Daten wurden gespeichert.")
print(f" Nächster Schritt: Ergebnisse in project-audit/data/gitlab-inventory/ prüfen")
except requests.exceptions.HTTPError as e:
if e.response.status_code == 401:
print("\n❌ Authentifizierung fehlgeschlagen. Bitte Token prüfen (read_api Scope nötig).")
elif e.response.status_code == 404:
print(f"\n❌ Gruppe '{GROUP_PATH}' nicht gefunden. Pfad prüfen.")
else:
print(f"\n❌ HTTP-Fehler: {e}")
sys.exit(1)
except requests.exceptions.ConnectionError:
print(f"\n❌ Verbindung zu {GITLAB_URL} fehlgeschlagen. VPN aktiv?")
sys.exit(1)
if __name__ == "__main__":
main()
+132
View File
@@ -0,0 +1,132 @@
"""
INB-Analyse: Extrahiert Text aus den INB-PDFs und sucht nach Widerspruechen
zwischen Hauptdokument und Anhaengen.
Fokus:
- Expresstrassen (bekannter Widerspruch)
- NAÄ (Netzausgeloeste Aenderungen)
- Abrechnung / Trassenpreis
- Fristen und Annahme-Prozesse
- Schnittstellen-Definitionen
"""
import PyPDF2
from pathlib import Path
import re
DATA_DIR = Path("project-audit/data")
def extract_pdf(path):
"""Extrahiert Text aus PDF."""
text = ""
try:
with open(path, "rb") as f:
reader = PyPDF2.PdfReader(f)
print(f" {path.name}: {len(reader.pages)} Seiten")
for page in reader.pages:
t = page.extract_text()
if t:
text += t + "\n"
except Exception as e:
print(f" FEHLER bei {path.name}: {e}")
return text
# PDFs laden
print("=== INB-2027 Dokumente laden ===")
docs = {}
for pdf in sorted(DATA_DIR.glob("INB-2027*.pdf")):
docs[pdf.stem] = extract_pdf(pdf)
print(f"\nGesamt: {len(docs)} Dokumente, {sum(len(v) for v in docs.values())} Zeichen")
# Suchbegriffe die fuer pathOS relevant sind
SEARCH_TERMS = [
"Expresstrasse",
"express",
"Netzausgelöst",
"netzausgelöst",
"NAÄ",
"implizit",
"Annahme",
"Frist",
"Abrechnung",
"Trassenpreis",
"Schnittstelle",
"TAF",
"TAP",
"elektronisch",
"IT-System",
"Portal",
"Bestellung",
"Stornierung",
"Änderung",
"Gelegenheitsverkehr",
"20 Stunden",
"20h",
"Verkehrstag",
"Tageswechsel",
"Teilausfall",
"Umleitung",
]
print("\n=== Relevante Stellen pro Dokument ===")
for doc_name, text in docs.items():
print(f"\n--- {doc_name} ({len(text)} Zeichen) ---")
found_terms = {}
for term in SEARCH_TERMS:
matches = [m.start() for m in re.finditer(re.escape(term), text, re.IGNORECASE)]
if matches:
found_terms[term] = len(matches)
if found_terms:
for term, count in sorted(found_terms.items(), key=lambda x: -x[1]):
print(f" {term:<25} {count:>3}x")
else:
print(" (keine relevanten Begriffe gefunden)")
# Detailsuche: Expresstrassen
print("\n\n=== DETAIL: Expresstrassen ===")
for doc_name, text in docs.items():
matches = list(re.finditer(r'(?i)express.{0,200}', text))
if matches:
print(f"\n--- {doc_name} ---")
for i, m in enumerate(matches[:5]):
context = text[max(0, m.start()-50):m.start()+200].replace("\n", " ").strip()
print(f" [{i+1}] ...{context}...")
# Detailsuche: NAÄ / Netzausgeloeste Aenderungen
print("\n\n=== DETAIL: NAÄ / Netzausgelöste Änderungen ===")
for doc_name, text in docs.items():
matches = list(re.finditer(r'(?i)(netzausgelöst|NAÄ|netz.{0,5}ausgelöst).{0,200}', text))
if matches:
print(f"\n--- {doc_name} ---")
for i, m in enumerate(matches[:5]):
context = text[max(0, m.start()-30):m.start()+200].replace("\n", " ").strip()
print(f" [{i+1}] ...{context}...")
# Detailsuche: Fristen / Annahme
print("\n\n=== DETAIL: Fristen und Annahme ===")
for doc_name, text in docs.items():
matches = list(re.finditer(r'(?i)(Annahme|angenommen|Frist).{0,150}', text))
if matches:
print(f"\n--- {doc_name} ({len(matches)} Treffer, zeige 5) ---")
for i, m in enumerate(matches[:5]):
context = text[max(0, m.start()-20):m.start()+150].replace("\n", " ").strip()
print(f" [{i+1}] ...{context}...")
# Detailsuche: Abrechnung / Trassenpreis
print("\n\n=== DETAIL: Abrechnung / Trassenpreis ===")
for doc_name, text in docs.items():
matches = list(re.finditer(r'(?i)(Abrechnung|Trassenpreis|Entgelt|Faktur).{0,150}', text))
if matches:
print(f"\n--- {doc_name} ({len(matches)} Treffer, zeige 5) ---")
for i, m in enumerate(matches[:5]):
context = text[max(0, m.start()-20):m.start()+150].replace("\n", " ").strip()
print(f" [{i+1}] ...{context}...")
# Export als Textdateien fuer spaetere Analyse
print("\n\n=== Export als Text ===")
for doc_name, text in docs.items():
out_path = DATA_DIR / f"{doc_name}.txt"
with open(out_path, "w", encoding="utf-8") as f:
f.write(text)
print(f" {out_path.name}: {len(text)} Zeichen")
+43
View File
@@ -0,0 +1,43 @@
"""Gezielte Suche in INB-Hauptdokument nach pathOS-relevanten Themen."""
import re
with open("project-audit/data/INB-2027-Hauptdokument-data.txt", "r", encoding="utf-8") as f:
text = f.read()
searches = [
# Expresstrassen / Prioritaet
("Expresstrasse|Expressangebot|Express-Trasse|Vorrangtrasse|vorrangig|Priorität.*Trasse", "Expresstrassen/Vorrang"),
# Relevanz-Kategorien
("sehr hohe|hohe Relevanz|mittlere Relevanz|geringe Relevanz|Relevanz", "Relevanz-Kategorien"),
# 20h / Tageswechsel
("20 Stunden|20h|Tageswechsel|Verkehrstag.*wechsel|über.*Mitternacht", "20h/Tageswechsel"),
# NAÄ / implizite Annahme
("netzausgelöst|NAÄ|implizit.*Annahme|automatisch.*Annahme|Annahme.*Frist", "NAÄ/Implizite Annahme"),
# Abrechnung
("Abrechnung.*Trasse|Trassenentgelt|Fakturierung|Rechnungsstellung", "Abrechnung"),
# Stornierung
("Stornierung.*Frist|Stornogebühr|Stornierungsentgelt", "Stornierung/Fristen"),
# GelV spezifisch
("Gelegenheitsverkehr.*Frist|GelV|ad.hoc", "GelV/Ad-hoc"),
# Schnittstelle / Common Interface
("Common Interface|Schnittstelle.*TAF|Schnittstelle.*TAP|XSD|XML.*Nachricht", "Schnittstelle/CI"),
# Kapazitaet / Koordinierung
("Kapazitätskonflikt|Koordinierungsverfahren|Überlastung", "Kapazitaet"),
# Baubedingt
("baubedingt|Baumaßnahme|Sperrung.*Strecke", "Baubedingt"),
]
for pattern, label in searches:
matches = list(re.finditer(pattern, text, re.IGNORECASE))
if matches:
print(f"\n{'='*80}")
print(f" {label}: {len(matches)} Treffer")
print(f"{'='*80}")
for m in matches[:4]:
start = max(0, m.start() - 60)
end = min(len(text), m.start() + 250)
ctx = text[start:end].replace("\n", " ").strip()
# Markiere den Match
print(f"\n ...{ctx}...")
else:
print(f"\n {label}: KEINE TREFFER")
@@ -0,0 +1,76 @@
"""Sucht offene Abrechnungs-Tickets in TTTSol/TTTI."""
import urllib.request, json, ssl, urllib.parse
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
TOKEN = secrets.get("JIRA_TOKEN", "")
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
def search(jql):
url = f"https://arija.jaas.service.deutschebahn.com/rest/api/2/search?jql={urllib.parse.quote(jql)}&maxResults=30&fields=key,summary,status,priority,assignee,updated,labels"
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {TOKEN}")
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
return json.loads(resp.read().decode("utf-8"))
def print_issues(title, jql):
print(f"\n--- {title} ---")
r = search(jql)
total = r.get("total", 0)
print(f" Total: {total}")
for i in r.get("issues", [])[:20]:
f = i["fields"]
s = f["status"]["name"]
p = (f.get("priority") or {}).get("name", "?")
a = (f.get("assignee") or {}).get("displayName", "Unassigned")
labels = ", ".join(f.get("labels", []))
summary = f["summary"][:70]
print(f" {i['key']:<15} [{s:<14}] {p:<10} {summary}")
if a != "Unassigned" or labels:
extra = f"Assignee: {a}" if a != "Unassigned" else ""
if labels:
extra += f" Labels: {labels}"
print(f" {'':15} {extra}")
# Abrechnung offen
print_issues(
"Abrechnung (offen, TTTSol + TTTI)",
'text ~ "Abrechnung" AND (project = TTTSOL OR project = TTTI) AND statusCategory != Done ORDER BY priority DESC'
)
# AC Trasse
print_issues(
"AC Trasse (alle Projekte, offen)",
'text ~ "AC Trasse" AND statusCategory != Done ORDER BY priority DESC'
)
# O2CCIB-6931 direkt (implizite Annahme)
print_issues(
"O2CCIB-6931 - Implizite Annahme NAÄ",
'key = O2CCIB-6931'
)
# TTTSOL-2184 direkt (implizite Annahme Highest)
print_issues(
"TTTSOL-2184 - Umgang mit impliziter Annahme",
'key = TTTSOL-2184'
)
# TTTSOL-1677 (20h Tool)
print_issues(
"TTTSOL-1677 - 20h Tool",
'key = TTTSOL-1677'
)
# TTTSOL-2035 (Abrechnung Zugnummer Umleitungsfall)
print_issues(
"TTTSOL-2035 - Abrechnung Zugnummer Umleitungsfall",
'key = TTTSOL-2035'
)
@@ -0,0 +1,127 @@
<#
.SYNOPSIS
Exportiert Fachliches Board und TTTI Board aus Jira
#>
$JiraUrl = "https://arija.jaas.service.deutschebahn.com"
$ApiBase = "$JiraUrl/rest"
$OutputDir = "project-audit\data\jira-boards"
$Boards = @(
@{ id = 2791; name = "Fachliches-Board" }
@{ id = 1487; name = "TTTI-Board" }
)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "============================================================"
Write-Host " pathOS Boards Export (Fachlich + TTTI)"
Write-Host "============================================================"
$Token = Read-Host "Jira PAT"
$Headers = @{ "Authorization" = "Bearer $Token"; "Content-Type" = "application/json" }
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
function Invoke-JiraApi($Uri) {
try {
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
return ($R.Content | ConvertFrom-Json)
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
return $null
}
}
foreach ($Board in $Boards) {
$BId = $Board.id
$BName = $Board.name
Write-Host ""
Write-Host ">> $BName (Board ID: $BId)" -ForegroundColor Yellow
# Board-Info
$Info = Invoke-JiraApi "$ApiBase/agile/1.0/board/$BId"
if ($Info) {
Write-Host " Board: $($Info.name) (Projekt: $($Info.location.projectKey))"
$Info | ConvertTo-Json -Depth 5 | Out-File "$OutputDir\$BName-info.json" -Encoding UTF8
}
# Board-Config
$Config = Invoke-JiraApi "$ApiBase/agile/1.0/board/$BId/configuration"
if ($Config) {
$Config | ConvertTo-Json -Depth 10 | Out-File "$OutputDir\$BName-config.json" -Encoding UTF8
}
# Issues auf dem Board
Write-Host " Lade Issues..." -NoNewline
$Issues = @()
$Start = 0
do {
$Uri = "$ApiBase/agile/1.0/board/$BId/issue?startAt=$Start&maxResults=100&fields=summary,status,issuetype,assignee,labels,components,fixVersions,priority,created,updated"
$Result = Invoke-JiraApi $Uri
if ($null -eq $Result) { break }
$Issues += $Result.issues
$Start += 100
Write-Host "." -NoNewline
} while ($Issues.Count -lt $Result.total -and $Issues.Count -lt 1000)
Write-Host ""
Write-Host " $($Issues.Count) Issues geladen" -ForegroundColor Green
$Issues | ConvertTo-Json -Depth 10 | Out-File "$OutputDir\$BName-issues.json" -Encoding UTF8
# Analyse
$ByType = $Issues | Group-Object { $_.fields.issuetype.name }
$ByStatus = $Issues | Group-Object { $_.fields.status.name }
$ByLabel = @{}
foreach ($I in $Issues) {
if ($I.fields.labels) {
foreach ($L in $I.fields.labels) { $ByLabel[$L] = ($ByLabel[$L]) + 1 }
}
}
$Done = ($Issues | Where-Object { $_.fields.status.name -match "Fertig|Done|Closed|Resolved|Geloest" }).Count
$InProg = ($Issues | Where-Object { $_.fields.status.name -match "Implementation|In Bearbeitung|In Progress|Review|Validierung|Analysis" }).Count
$Open = $Issues.Count - $Done - $InProg
Write-Host " Fertig: $Done | In Arbeit: $InProg | Offen: $Open"
# CSV
$CsvData = $Issues | ForEach-Object {
$Comps = if ($_.fields.components) { ($_.fields.components | ForEach-Object { $_.name }) -join ", " } else { "" }
$Labels = if ($_.fields.labels) { $_.fields.labels -join ", " } else { "" }
[PSCustomObject]@{
Key = $_.key
Typ = $_.fields.issuetype.name
Status = $_.fields.status.name
Prioritaet = if ($_.fields.priority) { $_.fields.priority.name } else { "" }
Zusammenfassung = $_.fields.summary
Komponenten = $Comps
Labels = $Labels
Assignee = if ($_.fields.assignee) { $_.fields.assignee.displayName } else { "" }
Erstellt = $_.fields.created
Aktualisiert = $_.fields.updated
}
}
$CsvData | Export-Csv "$OutputDir\$BName-issues.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
# Markdown
$Md = @()
$Md += "# $BName Analyse"
$Md += ""
$Md += "> Board ID: $BId | Issues: $($Issues.Count) | Fertig: $Done | In Arbeit: $InProg | Offen: $Open"
$Md += ""
$Md += "## Nach Typ"
foreach ($G in ($ByType | Sort-Object Count -Descending)) { $Md += "- $($G.Name): $($G.Count)" }
$Md += ""
$Md += "## Nach Status"
foreach ($G in ($ByStatus | Sort-Object Count -Descending)) { $Md += "- $($G.Name): $($G.Count)" }
$Md += ""
$Md += "## Top Labels"
foreach ($E in ($ByLabel.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 20)) { $Md += "- $($E.Key): $($E.Value)" }
$Md -join "`n" | Out-File "$OutputDir\$BName-analyse.md" -Encoding UTF8
}
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " FERTIG! Ergebnisse in $OutputDir" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
@@ -0,0 +1,64 @@
"""Prueft BSSUPPORT Projekt-Struktur und sucht gefixt Bugs."""
import urllib.request, json, ssl, urllib.parse
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
TOKEN = secrets.get("JIRA_TOKEN", "")
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
def search(jql, max_results=30):
url = f"https://arija.jaas.service.deutschebahn.com/rest/api/2/search?jql={urllib.parse.quote(jql)}&maxResults={max_results}&fields=key,summary,status,issuetype,updated,resolved,resolution,fixVersions,labels"
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {TOKEN}")
try:
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
return json.loads(resp.read().decode("utf-8"))
except Exception as e:
print(f" API-Fehler: {e}")
return {"issues": [], "total": 0}
# 1. Alle kuerzlich aktualisierten Tickets
print("=== BSSUPPORT: Letzte 30 aktualisierte Tickets ===")
r = search("project = BSSUPPORT ORDER BY updated DESC")
print(f"Total im Projekt: {r.get('total', 0)}")
print()
types_seen = set()
for i in r.get("issues", []):
f = i["fields"]
itype = f.get("issuetype", {}).get("name", "?")
types_seen.add(itype)
s = f["status"]["name"]
res = (f.get("resolution") or {}).get("name", "")
fv = ", ".join([v["name"] for v in f.get("fixVersions", [])])
updated = f.get("updated", "")[:10]
print(f" {i['key']:<18} [{itype:<15}] [{s:<14}] Res={res:<12} Upd={updated} {f['summary'][:50]}")
if fv:
print(f" {'':18} FixVersion: {fv}")
print(f"\nGesehene Issue-Typen: {sorted(types_seen)}")
# 2. Nur geschlossene/geloeste in letzten 2 Wochen
print("\n\n=== BSSUPPORT: Geschlossen in letzten 14 Tagen ===")
r2 = search("project = BSSUPPORT AND statusCategory = Done AND updated >= -14d ORDER BY updated DESC")
print(f"Treffer: {r2.get('total', 0)}")
for i in r2.get("issues", []):
f = i["fields"]
itype = f.get("issuetype", {}).get("name", "?")
s = f["status"]["name"]
res = (f.get("resolution") or {}).get("name", "")
resolved = (f.get("resolved") or "")[:10]
fv = ", ".join([v["name"] for v in f.get("fixVersions", [])])
labels = ", ".join(f.get("labels", []))
print(f" {i['key']:<18} [{itype:<15}] Res={res:<12} Resolved={resolved} {f['summary'][:55]}")
if fv:
print(f" {'':18} FixVersion: {fv}")
if labels:
print(f" {'':18} Labels: {labels}")
@@ -0,0 +1,78 @@
"""
Sucht BSSUPPORT-Tickets die in den letzten 2 Wochen durch Releases gefixt wurden.
Nur Defects/Bugs.
"""
import urllib.request, json, ssl, urllib.parse
from datetime import datetime, timedelta
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
TOKEN = secrets.get("JIRA_TOKEN", "")
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
def search(jql, max_results=100):
url = f"https://arija.jaas.service.deutschebahn.com/rest/api/2/search?jql={urllib.parse.quote(jql)}&maxResults={max_results}&fields=key,summary,status,priority,assignee,updated,resolved,issuetype,labels,fixVersions,resolution"
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {TOKEN}")
try:
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
return json.loads(resp.read().decode("utf-8"))
except Exception as e:
print(f" API-Fehler: {e}")
return {"issues": [], "total": 0}
two_weeks_ago = (datetime.now() - timedelta(days=14)).strftime("%Y-%m-%d")
print(f"=== BSSUPPORT: Bugs/Defects gefixt seit {two_weeks_ago} ===")
print()
# Suche 1: Resolved in letzten 2 Wochen, Typ Bug/Defect
jql = f'project = BSSUPPORT AND issuetype in (Bug, Defect, Fehler) AND resolved >= "{two_weeks_ago}" ORDER BY resolved DESC'
r = search(jql)
print(f"Suche 1 (resolved >= {two_weeks_ago}, Bug/Defect): {r['total']} Treffer")
for i in r.get("issues", []):
f = i["fields"]
s = f["status"]["name"]
p = (f.get("priority") or {}).get("name", "?")
res = (f.get("resolution") or {}).get("name", "?")
resolved = f.get("resolved", "")[:10] if f.get("resolved") else "?"
fix_versions = ", ".join([v["name"] for v in f.get("fixVersions", [])])
print(f" {i['key']:<18} [{s:<14}] Res={res:<10} Resolved={resolved} {f['summary'][:55]}")
if fix_versions:
print(f" {'':18} FixVersion: {fix_versions}")
# Suche 2: Status Done/Geschlossen in letzten 2 Wochen (breiter)
print()
jql2 = f'project = BSSUPPORT AND issuetype in (Bug, Defect, Fehler, "Sub-task") AND statusCategory = Done AND updated >= "{two_weeks_ago}" ORDER BY updated DESC'
r2 = search(jql2)
print(f"Suche 2 (Done + updated >= {two_weeks_ago}): {r2['total']} Treffer")
for i in r2.get("issues", [])[:30]:
f = i["fields"]
s = f["status"]["name"]
itype = f.get("issuetype", {}).get("name", "?")
res = (f.get("resolution") or {}).get("name", "?")
fix_versions = ", ".join([v["name"] for v in f.get("fixVersions", [])])
print(f" {i['key']:<18} [{itype:<10}] [{s:<14}] Res={res:<10} {f['summary'][:55]}")
if fix_versions:
print(f" {'':18} FixVersion: {fix_versions}")
# Suche 3: Alle Typen die "fix" im Resolution haben
print()
jql3 = f'project = BSSUPPORT AND resolution = Fixed AND resolved >= "{two_weeks_ago}" ORDER BY resolved DESC'
r3 = search(jql3)
print(f"Suche 3 (resolution=Fixed, alle Typen): {r3['total']} Treffer")
for i in r3.get("issues", [])[:30]:
f = i["fields"]
itype = f.get("issuetype", {}).get("name", "?")
resolved = f.get("resolved", "")[:10] if f.get("resolved") else "?"
fix_versions = ", ".join([v["name"] for v in f.get("fixVersions", [])])
print(f" {i['key']:<18} [{itype:<10}] Resolved={resolved} {f['summary'][:55]}")
if fix_versions:
print(f" {'':18} FixVersion: {fix_versions}")
@@ -0,0 +1,117 @@
"""
BSSUPPORT Bugs mit Releases in KW 18+19 (28.04 - 07.05.2026).
Sucht Bugs die:
- FixVersion enthalten (KTU oder Prod Release)
- In den letzten 2 Kalenderwochen aktualisiert wurden
- Status: Fertig/Geschlossen ODER ReTest (= im Release enthalten)
"""
import urllib.request, json, ssl, urllib.parse
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
TOKEN = secrets.get("JIRA_TOKEN", "")
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
JIRA_URL = "https://arija.jaas.service.deutschebahn.com"
def search(jql, max_results=100):
url = f"{JIRA_URL}/rest/api/2/search?jql={urllib.parse.quote(jql)}&maxResults={max_results}&fields=key,summary,status,issuetype,updated,resolved,resolution,fixVersions,labels,description"
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {TOKEN}")
try:
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
return json.loads(resp.read().decode("utf-8"))
except Exception as e:
print(f" API-Fehler: {e}")
return {"issues": [], "total": 0}
# KW18 Start: Montag 28.04.2026
START_DATE = "2026-04-28"
print(f"=== BSSUPPORT: Bugs mit Release-Bezug seit {START_DATE} (KW18+19) ===")
print()
# Suche: Bugs mit FixVersion die in KW18/19 aktualisiert wurden
jql = f'project = BSSUPPORT AND issuetype = Bug AND fixVersions is not EMPTY AND updated >= "{START_DATE}" ORDER BY fixVersions DESC, updated DESC'
r = search(jql)
print(f"Bugs mit FixVersion (aktualisiert seit {START_DATE}): {r.get('total', 0)}")
print()
# Auch Bugs im ReTest (= im Release, noch nicht abgenommen)
jql2 = f'project = BSSUPPORT AND issuetype = Bug AND status = ReTest AND updated >= "{START_DATE}" ORDER BY updated DESC'
r2 = search(jql2)
# Auch geschlossene Bugs ohne FixVersion aber mit Release-Bezug im Label
jql3 = f'project = BSSUPPORT AND issuetype = Bug AND statusCategory = Done AND updated >= "{START_DATE}" ORDER BY updated DESC'
r3 = search(jql3)
# Alle zusammenfuehren (deduplizieren)
all_issues = {}
for issue_list in [r.get("issues", []), r2.get("issues", []), r3.get("issues", [])]:
for i in issue_list:
all_issues[i["key"]] = i
print(f"Gesamt (dedupliziert): {len(all_issues)} Bugs")
print()
# Ausgabe als Tabelle
print(f"{'Ticket':<18} {'Titel':<55} {'Release':<20} {'Status'}")
print("=" * 110)
for key in sorted(all_issues.keys()):
i = all_issues[key]
f = i["fields"]
s = f["status"]["name"]
res = (f.get("resolution") or {}).get("name", "")
fv = ", ".join([v["name"] for v in f.get("fixVersions", [])])
summary = f["summary"][:54]
# Status-Anzeige
if s == "Geschlossen" and res == "Fertig":
status_display = "Fertig"
elif s == "Geschlossen" and res:
status_display = res
elif s == "ReTest":
status_display = "ReTest"
else:
status_display = s
# Nur KTU/Prod Releases oder geschlossene Bugs
if fv or status_display in ["Fertig", "ReTest"]:
link = f"{JIRA_URL}/browse/{key}"
print(f" {key:<16} {summary:<55} {fv:<20} {status_display}")
print()
print()
print("=== Markdown-Tabelle ===")
print()
print("| Ticket | Titel | Release | Status |")
print("|--------|-------|---------|--------|")
for key in sorted(all_issues.keys()):
i = all_issues[key]
f = i["fields"]
s = f["status"]["name"]
res = (f.get("resolution") or {}).get("name", "")
fv = ", ".join([v["name"] for v in f.get("fixVersions", [])])
summary = f["summary"][:65]
if s == "Geschlossen" and res == "Fertig":
status_display = "✅ Fertig"
elif s == "Geschlossen" and res:
status_display = f"🔒 {res}"
elif s == "ReTest":
status_display = "🔄 ReTest"
else:
status_display = s
if fv or "Fertig" in status_display or "ReTest" in status_display:
link = f"[{key}]({JIRA_URL}/browse/{key})"
print(f"| {link} | {summary} | {fv} | {status_display} |")
@@ -0,0 +1,154 @@
<#
.SYNOPSIS
Bug-Verursachungs-Analyse (Bulk-Ansatz)
Holt alle Bugs + Features der letzten 6 Monate in 2 Bulk-Abfragen
und korreliert lokal.
#>
$JiraUrl = "https://arija.jaas.service.deutschebahn.com"
$ApiBase = "$JiraUrl/rest"
$OutputDir = "project-audit\data\jira-bugs"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "============================================================"
Write-Host " Bug-Verursachungs-Analyse (Bulk)"
Write-Host " 2 API-Calls statt 60+"
Write-Host "============================================================"
$Token = $null
$SecretsPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\.secrets"
if (Test-Path $SecretsPath) {
Get-Content $SecretsPath | ForEach-Object {
if ($_ -match "^JIRA_TOKEN=(.+)$") {
$val = $Matches[1].Trim()
if ($val -ne "HIER_TOKEN_EINTRAGEN") { $Token = $val }
}
}
}
if (-not $Token) { $Token = Read-Host "Jira PAT" }
$Headers = @{ "Authorization" = "Bearer $Token"; "Content-Type" = "application/json" }
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
function Invoke-JiraBulk {
param([string]$Jql, [int]$MaxTotal = 5000)
$AllIssues = @()
$StartAt = 0
$PageSize = 1000
do {
$Uri = "$ApiBase/api/2/search?jql=$([Uri]::EscapeDataString($Jql))&startAt=$StartAt&maxResults=$PageSize&fields=summary,created,resolutiondate,assignee,issuetype,project,reporter"
Write-Host " Lade $StartAt..." -NoNewline
try {
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
$Data = $R.Content | ConvertFrom-Json
$AllIssues += $Data.issues
$Total = $Data.total
Write-Host " $($AllIssues.Count)/$Total" -ForegroundColor Green
$StartAt += $PageSize
Start-Sleep -Seconds 2
} catch {
$StatusCode = 0
if ($_.Exception.Response) { $StatusCode = [int]$_.Exception.Response.StatusCode }
if ($StatusCode -eq 429) {
Write-Host " [429 - warte 30s]" -ForegroundColor Yellow
Start-Sleep -Seconds 30
} else {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
break
}
}
} while ($AllIssues.Count -lt $Total -and $AllIssues.Count -lt $MaxTotal)
return $AllIssues
}
$Projects = "O2C404, O2CCIB, O2CZERO"
# Abfrage 1: Alle Bugs (6 Monate)
Write-Host ""
Write-Host ">> Bugs (6 Monate)..." -ForegroundColor Yellow
$BugJql = "project in ($Projects) AND issuetype = Bug AND created >= -180d ORDER BY created DESC"
$Bugs = Invoke-JiraBulk -Jql $BugJql -MaxTotal 2000
Write-Host " $($Bugs.Count) Bugs geladen"
# Abfrage 2: Abgeschlossene Features/Stories (6 Monate)
Write-Host ""
Write-Host ">> Features/Stories (6 Monate, abgeschlossen)..." -ForegroundColor Yellow
$FeatureJql = "project in ($Projects) AND issuetype in (Feature, Enabler, Story, Aufgabe, Unteraufgabe) AND status in (Fertig, Done, Closed) AND resolved >= -180d ORDER BY resolved DESC"
$Features = Invoke-JiraBulk -Jql $FeatureJql -MaxTotal 3000
Write-Host " $($Features.Count) Features geladen"
# Lokale Analyse pro Team
Write-Host ""
Write-Host ">> Korrelationsanalyse..." -ForegroundColor Yellow
foreach ($ProjectKey in @("O2C404", "O2CCIB", "O2CZERO")) {
$TeamName = switch ($ProjectKey) { "O2C404" { "Team 404" }; "O2CCIB" { "Team CIB" }; "O2CZERO" { "Team Zero" } }
Write-Host ""
Write-Host " === $TeamName ===" -ForegroundColor Cyan
$TeamBugs = $Bugs | Where-Object { $_.fields.project.key -eq $ProjectKey }
$TeamFeatures = $Features | Where-Object { $_.fields.project.key -eq $ProjectKey }
Write-Host " $($TeamBugs.Count) Bugs, $($TeamFeatures.Count) Features"
# Korrelation: Wer hat in 14 Tagen vor Bug-Erstellung Features abgeschlossen?
$Causation = @{}
foreach ($Bug in $TeamBugs) {
$BugCreated = [DateTime]::Parse($Bug.fields.created)
$WindowStart = $BugCreated.AddDays(-14)
$RecentFeatures = $TeamFeatures | Where-Object {
$_.fields.resolutiondate -and
[DateTime]::Parse($_.fields.resolutiondate) -ge $WindowStart -and
[DateTime]::Parse($_.fields.resolutiondate) -le $BugCreated
}
foreach ($F in $RecentFeatures) {
if ($F.fields.assignee) {
$Dev = $F.fields.assignee.displayName
if (-not $Causation[$Dev]) { $Causation[$Dev] = @{ Features = 0; BugKeys = @() } }
if ($Causation[$Dev].BugKeys -notcontains $Bug.key) {
$Causation[$Dev].BugKeys += $Bug.key
}
}
}
}
# Feature-Count pro Dev
foreach ($F in $TeamFeatures) {
if ($F.fields.assignee) {
$Dev = $F.fields.assignee.displayName
if (-not $Causation[$Dev]) { $Causation[$Dev] = @{ Features = 0; BugKeys = @() } }
$Causation[$Dev].Features++
}
}
# Ausgabe Top 10
Write-Host " --- Potenzielle Bug-Verursachung (14-Tage-Fenster) ---"
$Sorted = $Causation.GetEnumerator() | Sort-Object { $_.Value.BugKeys.Count } -Descending
foreach ($E in ($Sorted | Select-Object -First 10)) {
$UniqueBugs = $E.Value.BugKeys.Count
$Feats = $E.Value.Features
$Rate = if ($Feats -gt 0) { [Math]::Round($UniqueBugs / $Feats * 100, 0) } else { 0 }
Write-Host (" {0,-30} Features:{1,4} | Bugs:{2,4} | Rate:{3,3}%" -f $E.Key, $Feats, $UniqueBugs, $Rate)
}
# CSV
$CsvData = $Sorted | ForEach-Object {
[PSCustomObject]@{
Team = $TeamName
Developer = $_.Key
Features = $_.Value.Features
CorrelatedBugs = $_.Value.BugKeys.Count
BugRate = if ($_.Value.Features -gt 0) { [Math]::Round($_.Value.BugKeys.Count / $_.Value.Features * 100, 0) } else { 0 }
}
}
$CsvData | Export-Csv "$OutputDir\$ProjectKey-causation.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
}
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " FERTIG! API-Calls: ~$([Math]::Ceiling(($Bugs.Count + $Features.Count) / 1000) + 2)" -ForegroundColor Green
Write-Host " Ergebnisse in $OutputDir" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
@@ -0,0 +1,114 @@
"""
Systematischer Scan nach geschaeftskritischen Themen in Jira.
Strategie: Suche nach Signalen die auf politische/regulatorische/kundenrelevante Risiken hindeuten.
Signale:
1. Labels: TopThema, Konzernreporting, Taskforce-*, TTT_offenePunkte
2. Prioritaet: Highest + Blocked
3. Eskalation im Titel/Summary
4. Kundenrelevant: DBFernverkehr, DBCargo, DBRegio, TTT_Kunde
5. Regulatorisch: BNetzA, ERegG, SNB
6. Lange offen + hohe Prio
"""
import urllib.request, json, ssl, urllib.parse
from collections import defaultdict
from datetime import datetime
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
TOKEN = secrets.get("JIRA_TOKEN", "")
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
def search(jql, max_results=50):
url = f"https://arija.jaas.service.deutschebahn.com/rest/api/2/search?jql={urllib.parse.quote(jql)}&maxResults={max_results}&fields=key,summary,status,priority,assignee,updated,created,labels,issuetype"
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {TOKEN}")
try:
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
return json.loads(resp.read().decode("utf-8"))
except Exception as e:
print(f" API-Fehler: {e}")
return {"issues": [], "total": 0}
def print_issues(issues, max_show=15):
for i in issues[:max_show]:
f = i["fields"]
s = f["status"]["name"]
p = (f.get("priority") or {}).get("name", "?")
labels = f.get("labels", [])
key_labels = [l for l in labels if any(x in l for x in ["TopThema", "Konzern", "Taskforce", "Kunde", "offenePunkte", "Eskalation", "MUSS"])]
label_str = " [" + ", ".join(key_labels) + "]" if key_labels else ""
print(f" {i['key']:<15} [{s:<14}] {p:<10} {f['summary'][:60]}{label_str}")
print("=" * 100)
print("SYSTEMATISCHER SCAN: Geschaeftskritische Risiken")
print("=" * 100)
# 1. TopThema Label
print("\n--- 1. Label 'TopThema' (alle offenen) ---")
r = search('labels = TopThema AND statusCategory != Done ORDER BY priority DESC')
print(f" Total: {r['total']}")
print_issues(r["issues"])
# 2. Konzernreporting
print("\n--- 2. Label 'Konzernreporting' (offen) ---")
r = search('labels = Konzernreporting AND statusCategory != Done ORDER BY priority DESC')
print(f" Total: {r['total']}")
print_issues(r["issues"])
# 3. Taskforce-FplW (Fahrplanwechsel)
print("\n--- 3. Label 'Taskforce-FplW' (offen) ---")
r = search('labels = "Taskforce-FplW" AND statusCategory != Done ORDER BY priority DESC')
print(f" Total: {r['total']}")
print_issues(r["issues"])
# 4. Blocked + Highest/High
print("\n--- 4. Blocked + Highest/High Prioritaet ---")
r = search('status = Blocked AND priority in (Highest, High, Zeitkritisch, Hoch) AND (project = TTTSOL OR project = TTTI OR project = O2CCIB OR project = O2C404 OR project = O2CZERO) ORDER BY priority DESC')
print(f" Total: {r['total']}")
print_issues(r["issues"])
# 5. Eskalation im Titel
print("\n--- 5. 'Eskalation' im Titel (offen) ---")
r = search('summary ~ "Eskalation" AND statusCategory != Done ORDER BY updated DESC')
print(f" Total: {r['total']}")
print_issues(r["issues"])
# 6. Kundenrelevant (DBFernverkehr, DBCargo, DBRegio)
print("\n--- 6. Kundenrelevant (DB Fernverkehr/Cargo/Regio, offen) ---")
r = search('labels in (DBFernverkehr, DBCargo, DBRegio) AND statusCategory != Done ORDER BY priority DESC')
print(f" Total: {r['total']}")
print_issues(r["issues"])
# 7. TTT_Kunde Label
print("\n--- 7. Label 'TTT_Kunde' (offen) ---")
r = search('labels = TTT_Kunde AND statusCategory != Done ORDER BY priority DESC')
print(f" Total: {r['total']}")
print_issues(r["issues"])
# 8. TTT_offenePunkte + Highest
print("\n--- 8. TTT_offenePunkte + Highest ---")
r = search('labels = TTT_offenePunkte AND priority = Highest AND statusCategory != Done ORDER BY updated DESC')
print(f" Total: {r['total']}")
print_issues(r["issues"])
# 9. MUSS-Label (regulatorisch)
print("\n--- 9. Label 'TTT_MUSS' (offen) ---")
r = search('labels = TTT_MUSS AND statusCategory != Done ORDER BY priority DESC', 30)
print(f" Total: {r['total']}")
print_issues(r["issues"], 10)
# 10. Aelteste offene Highest-Tickets
print("\n--- 10. Aelteste offene Highest-Tickets (>60 Tage) ---")
r = search('priority = Highest AND statusCategory != Done AND (project = TTTSOL OR project = TTTI OR project = O2CCIB OR project = O2C404) AND created <= -60d ORDER BY created ASC')
print(f" Total: {r['total']}")
print_issues(r["issues"])
print("\n" + "=" * 100)
+186
View File
@@ -0,0 +1,186 @@
<#
.SYNOPSIS
Exportiert Jira-Daten fuer pathOS (O2CBS Projekt)
.DESCRIPTION
Zieht Boards, Sprints, Backlog-Items und Team-Zuordnungen.
Kompatibel mit PowerShell 5.1.
.USAGE
powershell -ExecutionPolicy Bypass -File ".\project-audit\scripts\jira-export.ps1"
#>
$JiraUrl = "https://arija.jaas.service.deutschebahn.com"
$ProjectKey = "O2CBS"
$ApiBase = "$JiraUrl/rest"
$OutputDir = "project-audit\data\jira-export"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "============================================================"
Write-Host " pathOS Jira Export - Projekt $ProjectKey"
Write-Host "============================================================"
Write-Host ""
$Token = Read-Host "Jira PAT (Personal Access Token)"
$Headers = @{ "Authorization" = "Bearer $Token"; "Content-Type" = "application/json" }
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
function Invoke-JiraApi {
param([string]$Uri)
try {
$Resp = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
return ($Resp.Content | ConvertFrom-Json)
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
return $null
}
}
# 1. Projekt-Info
Write-Host ">> Projekt-Info..." -ForegroundColor Yellow
$Project = Invoke-JiraApi "$ApiBase/api/2/project/$ProjectKey"
if ($null -eq $Project) { Write-Host "Verbindung fehlgeschlagen. Token pruefen." -ForegroundColor Red; exit 1 }
Write-Host " Projekt: $($Project.name) ($($Project.key))" -ForegroundColor Green
# 2. Boards finden
Write-Host ">> Boards suchen..." -ForegroundColor Yellow
$Boards = Invoke-JiraApi "$ApiBase/agile/1.0/board?projectKeyOrId=$ProjectKey&maxResults=50"
if ($Boards -and $Boards.values) {
Write-Host " $($Boards.values.Count) Boards gefunden" -ForegroundColor Green
$Boards.values | ForEach-Object { Write-Host " - $($_.name) (ID: $($_.id), Typ: $($_.type))" }
$Boards | ConvertTo-Json -Depth 5 | Out-File "$OutputDir\boards.json" -Encoding UTF8
}
# 3. Aktuelle Sprints pro Board
Write-Host ">> Sprints laden..." -ForegroundColor Yellow
$AllSprints = @()
if ($Boards -and $Boards.values) {
foreach ($Board in $Boards.values) {
$Sprints = Invoke-JiraApi "$ApiBase/agile/1.0/board/$($Board.id)/sprint?state=active,closed&maxResults=10"
if ($Sprints -and $Sprints.values) {
foreach ($S in $Sprints.values) {
$S | Add-Member -NotePropertyName "boardName" -NotePropertyValue $Board.name -Force
$S | Add-Member -NotePropertyName "boardId" -NotePropertyValue $Board.id -Force
$AllSprints += $S
}
}
}
}
Write-Host " $($AllSprints.Count) Sprints gefunden" -ForegroundColor Green
$AllSprints | ConvertTo-Json -Depth 5 | Out-File "$OutputDir\sprints.json" -Encoding UTF8
# 4. Backlog-Items mit Team-Labels (JQL)
Write-Host ">> Backlog-Items laden (letzte 2 PIs)..." -ForegroundColor Yellow
$Jql = "project = $ProjectKey AND updated >= -90d ORDER BY updated DESC"
$StartAt = 0
$AllIssues = @()
do {
$SearchUri = "$ApiBase/api/2/search?jql=$([Uri]::EscapeDataString($Jql))&startAt=$StartAt&maxResults=100&fields=summary,status,issuetype,assignee,labels,components,fixVersions,priority,created,updated,customfield_10100"
$Result = Invoke-JiraApi $SearchUri
if ($null -eq $Result) { break }
$AllIssues += $Result.issues
$StartAt += 100
Write-Host " $($AllIssues.Count) / $($Result.total) Issues geladen..."
} while ($AllIssues.Count -lt $Result.total -and $AllIssues.Count -lt 1000)
Write-Host " $($AllIssues.Count) Issues geladen" -ForegroundColor Green
$AllIssues | ConvertTo-Json -Depth 10 | Out-File "$OutputDir\issues.json" -Encoding UTF8
# 5. Komponenten (= Teams?)
Write-Host ">> Komponenten laden..." -ForegroundColor Yellow
$Components = Invoke-JiraApi "$ApiBase/api/2/project/$ProjectKey/components"
if ($Components) {
Write-Host " $($Components.Count) Komponenten" -ForegroundColor Green
$Components | ForEach-Object { Write-Host " - $($_.name) (Lead: $(if ($_.lead) { $_.lead.displayName } else { '-' }))" }
$Components | ConvertTo-Json -Depth 5 | Out-File "$OutputDir\components.json" -Encoding UTF8
}
# 6. Versionen (Releases)
Write-Host ">> Versionen laden..." -ForegroundColor Yellow
$Versions = Invoke-JiraApi "$ApiBase/api/2/project/$ProjectKey/versions"
if ($Versions) {
$RecentVersions = $Versions | Where-Object { $_.released -eq $true -or $_.archived -eq $false } | Sort-Object -Property releaseDate -Descending | Select-Object -First 20
Write-Host " $($Versions.Count) Versionen ($($RecentVersions.Count) aktuelle)" -ForegroundColor Green
$Versions | ConvertTo-Json -Depth 5 | Out-File "$OutputDir\versions.json" -Encoding UTF8
}
# 7. Analyse: Issues nach Team/Komponente
Write-Host ""
Write-Host ">> Analyse..." -ForegroundColor Yellow
$ByType = $AllIssues | Group-Object { $_.fields.issuetype.name }
$ByStatus = $AllIssues | Group-Object { $_.fields.status.name }
$ByComponent = @{}
foreach ($Issue in $AllIssues) {
if ($Issue.fields.components) {
foreach ($Comp in $Issue.fields.components) {
$ByComponent[$Comp.name] = ($ByComponent[$Comp.name]) + 1
}
}
}
$ByLabel = @{}
foreach ($Issue in $AllIssues) {
if ($Issue.fields.labels) {
foreach ($Label in $Issue.fields.labels) {
$ByLabel[$Label] = ($ByLabel[$Label]) + 1
}
}
}
# Ergebnis-Markdown
$Md = @()
$Md += "# Jira-Analyse pathOS ($ProjectKey)"
$Md += ""
$Md += "> Exportiert: $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
$Md += "> Issues (letzte 90 Tage): $($AllIssues.Count)"
$Md += ""
$Md += "## Issues nach Typ"
foreach ($G in ($ByType | Sort-Object Count -Descending)) { $Md += "- $($G.Name): $($G.Count)" }
$Md += ""
$Md += "## Issues nach Status"
foreach ($G in ($ByStatus | Sort-Object Count -Descending)) { $Md += "- $($G.Name): $($G.Count)" }
$Md += ""
$Md += "## Issues nach Komponente (Team)"
foreach ($E in ($ByComponent.GetEnumerator() | Sort-Object Value -Descending)) { $Md += "- $($E.Key): $($E.Value)" }
$Md += ""
$Md += "## Issues nach Label"
foreach ($E in ($ByLabel.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 30)) { $Md += "- $($E.Key): $($E.Value)" }
$Md += ""
$Md += "## Boards"
if ($Boards -and $Boards.values) { foreach ($B in $Boards.values) { $Md += "- $($B.name) (ID: $($B.id), Typ: $($B.type))" } }
$Md += ""
$Md += "## Aktuelle Sprints"
foreach ($S in ($AllSprints | Sort-Object startDate -Descending | Select-Object -First 10)) {
$Md += "- $($S.name) | Board: $($S.boardName) | Status: $($S.state) | $($S.startDate) - $($S.endDate)"
}
$Md += ""
$Md += "## Letzte Releases"
if ($RecentVersions) { foreach ($V in $RecentVersions) { $Md += "- $($V.name) | Released: $($V.released) | Datum: $($V.releaseDate)" } }
$Md -join "`n" | Out-File "$OutputDir\jira-analyse.md" -Encoding UTF8
# CSV fuer Issues
$CsvData = $AllIssues | ForEach-Object {
$Comps = if ($_.fields.components) { ($_.fields.components | ForEach-Object { $_.name }) -join ", " } else { "" }
$Labels = if ($_.fields.labels) { $_.fields.labels -join ", " } else { "" }
$Fix = if ($_.fields.fixVersions) { ($_.fields.fixVersions | ForEach-Object { $_.name }) -join ", " } else { "" }
[PSCustomObject]@{
Key = $_.key
Typ = $_.fields.issuetype.name
Status = $_.fields.status.name
Prioritaet = if ($_.fields.priority) { $_.fields.priority.name } else { "" }
Zusammenfassung = $_.fields.summary
Komponenten = $Comps
Labels = $Labels
FixVersion = $Fix
Assignee = if ($_.fields.assignee) { $_.fields.assignee.displayName } else { "" }
Erstellt = $_.fields.created
Aktualisiert = $_.fields.updated
}
}
$CsvData | Export-Csv "$OutputDir\issues.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " FERTIG! Ergebnisse in $OutputDir" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
@@ -0,0 +1,138 @@
<#
.SYNOPSIS
Findet das aelteste und neueste Ticket pro Person (Tenure-Analyse)
.NOTES
BULK-Ansatz: Nur 2-3 API-Calls statt 70+
Holt ALLE Issues der relevanten Projekte und gruppiert lokal nach Assignee
#>
$JiraUrl = "https://arija.jaas.service.deutschebahn.com"
$ApiBase = "$JiraUrl/rest"
$OutputDir = "project-audit\data\jira-bugs"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "============================================================"
Write-Host " Tenure-Analyse (Bulk-Abfrage)"
Write-Host " Strategie: Alle Issues holen, lokal nach Assignee gruppieren"
Write-Host "============================================================"
$Token = $null
$SecretsPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\.secrets"
if (Test-Path $SecretsPath) {
Get-Content $SecretsPath | ForEach-Object {
if ($_ -match "^JIRA_TOKEN=(.+)$") {
$val = $Matches[1].Trim()
if ($val -ne "HIER_TOKEN_EINTRAGEN") { $Token = $val }
}
}
}
if (-not $Token) { $Token = Read-Host "Jira PAT" }
$Headers = @{ "Authorization" = "Bearer $Token"; "Content-Type" = "application/json" }
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
function Invoke-JiraBulk {
param([string]$Jql, [int]$MaxTotal = 5000)
$AllIssues = @()
$StartAt = 0
$PageSize = 1000
do {
$Uri = "$ApiBase/api/2/search?jql=$([Uri]::EscapeDataString($Jql))&startAt=$StartAt&maxResults=$PageSize&fields=assignee,created,updated,project"
Write-Host " Lade Issues $StartAt - $($StartAt + $PageSize)..." -NoNewline
try {
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
$Data = $R.Content | ConvertFrom-Json
$AllIssues += $Data.issues
$Total = $Data.total
Write-Host " $($AllIssues.Count)/$Total" -ForegroundColor Green
$StartAt += $PageSize
Start-Sleep -Seconds 2
} catch {
$StatusCode = 0
if ($_.Exception.Response) { $StatusCode = [int]$_.Exception.Response.StatusCode }
if ($StatusCode -eq 429) {
Write-Host " [429 - warte 30s]" -ForegroundColor Yellow
Start-Sleep -Seconds 30
} else {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
break
}
}
} while ($AllIssues.Count -lt $Total -and $AllIssues.Count -lt $MaxTotal)
return $AllIssues
}
# EINE Bulk-Abfrage: Alle Issues aus allen Team-Projekten
$Projects = "O2C404, O2CCIB, O2CZERO, O2COS, O2CDEVOPS, O2CSYS, O2CBS, O2CQST, TTTI"
$Jql = "project in ($Projects) ORDER BY created ASC"
Write-Host ""
Write-Host ">> Bulk-Abfrage: Alle Issues aus $Projects" -ForegroundColor Yellow
$AllIssues = Invoke-JiraBulk -Jql $Jql -MaxTotal 25000
Write-Host ""
Write-Host " Gesamt: $($AllIssues.Count) Issues geladen" -ForegroundColor Cyan
# Lokal nach Assignee gruppieren
Write-Host ""
Write-Host ">> Gruppiere nach Assignee..." -ForegroundColor Yellow
$ByAssignee = @{}
foreach ($Issue in $AllIssues) {
if ($Issue.fields.assignee) {
$Name = $Issue.fields.assignee.displayName
if (-not $ByAssignee[$Name]) {
$ByAssignee[$Name] = @{ Issues = @(); FirstCreated = $null; LastUpdated = $null }
}
$Created = $Issue.fields.created.Substring(0, 10)
$Updated = $Issue.fields.updated.Substring(0, 10)
$ByAssignee[$Name].Issues += $Issue.key
if (-not $ByAssignee[$Name].FirstCreated -or $Created -lt $ByAssignee[$Name].FirstCreated) {
$ByAssignee[$Name].FirstCreated = $Created
$ByAssignee[$Name].FirstTicket = $Issue.key
$ByAssignee[$Name].FirstProject = $Issue.fields.project.key
}
if (-not $ByAssignee[$Name].LastUpdated -or $Updated -gt $ByAssignee[$Name].LastUpdated) {
$ByAssignee[$Name].LastUpdated = $Updated
$ByAssignee[$Name].LastTicket = $Issue.key
}
}
}
Write-Host " $($ByAssignee.Count) verschiedene Assignees gefunden"
# Ergebnisse aufbereiten
$Results = @()
foreach ($Entry in ($ByAssignee.GetEnumerator() | Sort-Object { $_.Value.FirstCreated })) {
$Name = $Entry.Key
$Data = $Entry.Value
$DaysAgo = ((Get-Date) - [DateTime]::Parse($Data.LastUpdated)).Days
$Status = if ($DaysAgo -le 30) { "Aktiv" } elseif ($DaysAgo -le 90) { "Wenig aktiv" } else { "INAKTIV ($DaysAgo Tage)" }
$Results += [PSCustomObject]@{
Person = $Name
FirstTicket = $Data.FirstTicket
FirstDate = $Data.FirstCreated
LastDate = $Data.LastUpdated
LastTicket = $Data.LastTicket
Status = $Status
Project = $Data.FirstProject
IssueCount = $Data.Issues.Count
}
}
# CSV speichern
$Results | Export-Csv "$OutputDir\tenure.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
# Ausgabe
Write-Host ""
Write-Host "=== Sortiert nach Startdatum (Top 40) ==="
$Results | Select-Object -First 40 | ForEach-Object {
$Color = if ($_.Status -eq "Aktiv") { "Green" } elseif ($_.Status -match "INAKTIV") { "Red" } else { "Yellow" }
Write-Host (" {0,-35} {1} bis {2} [{3}] ({4} Issues)" -f $_.Person, $_.FirstDate, $_.LastDate, $_.Status, $_.IssueCount) -ForegroundColor $Color
}
Write-Host ""
Write-Host "FERTIG! $($Results.Count) Personen in $OutputDir\tenure.csv" -ForegroundColor Green
Write-Host "API-Calls: ~$([Math]::Ceiling($AllIssues.Count / 1000) + 1) (statt $($ByAssignee.Count * 2))" -ForegroundColor Cyan
@@ -0,0 +1,132 @@
"""
Sucht nach den drei kritischen Themen in Jira:
1. Implizite Annahme NAÄ (Netzausgeloeste Aenderungen)
2. 20h Zug
3. Abrechnung / AC Trasse / TTT
"""
import urllib.request, json, ssl
from pathlib import Path
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
TOKEN = secrets.get("JIRA_TOKEN", "")
if not TOKEN:
print("FEHLER: JIRA_TOKEN nicht in .secrets!")
exit(1)
JIRA_URL = "https://arija.jaas.service.deutschebahn.com"
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
def jira_search(jql, max_results=50):
url = f"{JIRA_URL}/rest/api/2/search?jql={urllib.parse.quote(jql)}&maxResults={max_results}&fields=key,summary,status,priority,assignee,created,updated,issuetype,labels"
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("Content-Type", "application/json")
try:
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
return json.loads(resp.read().decode("utf-8"))
except Exception as e:
print(f" Fehler: {e}")
return None
import urllib.parse
print("=" * 80)
print("KRITISCHE THEMEN - Jira-Suche")
print("=" * 80)
# 1. Implizite Annahme / NAÄ
print("\n--- 1. Implizite Annahme / NAÄ / Netzausgelöste Änderungen ---")
queries_naa = [
'text ~ "implizite Annahme" ORDER BY updated DESC',
'text ~ "Netzausgelöste" ORDER BY updated DESC',
'text ~ "NAÄ" ORDER BY updated DESC',
'summary ~ "implizit" AND summary ~ "Annahme" ORDER BY updated DESC',
]
naa_issues = []
for jql in queries_naa:
result = jira_search(jql, 20)
if result and result.get("issues"):
for issue in result["issues"]:
key = issue["key"]
if key not in [i["key"] for i in naa_issues]:
naa_issues.append(issue)
print(f" Gefunden: {len(naa_issues)} Issues")
for issue in naa_issues[:15]:
fields = issue["fields"]
status = fields["status"]["name"]
prio = fields.get("priority", {}).get("name", "?")
assignee = fields.get("assignee", {})
assignee_name = assignee.get("displayName", "Unassigned") if assignee else "Unassigned"
print(f" {issue['key']:<15} [{status:<12}] {prio:<8} {fields['summary'][:60]}")
print(f" {'':15} Assignee: {assignee_name} Updated: {fields['updated'][:10]}")
# 2. 20h Zug / TTTSol
print("\n--- 2. 20h Zug ---")
queries_20h = [
'text ~ "20h Zug" ORDER BY updated DESC',
'text ~ "20-Stunden" ORDER BY updated DESC',
'project = TTTSol AND text ~ "20h" ORDER BY updated DESC',
'text ~ "20h" AND text ~ "Zug" ORDER BY updated DESC',
]
zug_issues = []
for jql in queries_20h:
result = jira_search(jql, 20)
if result and result.get("issues"):
for issue in result["issues"]:
key = issue["key"]
if key not in [i["key"] for i in zug_issues]:
zug_issues.append(issue)
print(f" Gefunden: {len(zug_issues)} Issues")
for issue in zug_issues[:15]:
fields = issue["fields"]
status = fields["status"]["name"]
prio = fields.get("priority", {}).get("name", "?")
assignee = fields.get("assignee", {})
assignee_name = assignee.get("displayName", "Unassigned") if assignee else "Unassigned"
print(f" {issue['key']:<15} [{status:<12}] {prio:<8} {fields['summary'][:60]}")
print(f" {'':15} Assignee: {assignee_name} Updated: {fields['updated'][:10]}")
# 3. Abrechnung / AC Trasse
print("\n--- 3. Abrechnung / AC Trasse ---")
queries_abr = [
'text ~ "Abrechnung" AND (project = TTTSol OR project = TTTI) AND status != Done ORDER BY priority DESC',
'text ~ "AC Trasse" ORDER BY updated DESC',
'summary ~ "Abrechnung" AND status != Done ORDER BY priority DESC',
]
abr_issues = []
for jql in queries_abr:
result = jira_search(jql, 30)
if result and result.get("issues"):
for issue in result["issues"]:
key = issue["key"]
if key not in [i["key"] for i in abr_issues]:
abr_issues.append(issue)
print(f" Gefunden: {len(abr_issues)} Issues")
for issue in abr_issues[:20]:
fields = issue["fields"]
status = fields["status"]["name"]
prio = fields.get("priority", {}).get("name", "?")
assignee = fields.get("assignee", {})
assignee_name = assignee.get("displayName", "Unassigned") if assignee else "Unassigned"
labels = ", ".join(fields.get("labels", []))
print(f" {issue['key']:<15} [{status:<12}] {prio:<8} {fields['summary'][:60]}")
if labels:
print(f" {'':15} Labels: {labels}")
print("\n" + "=" * 80)
print("ZUSAMMENFASSUNG")
print("=" * 80)
print(f" NAÄ/Implizite Annahme: {len(naa_issues)} Issues")
print(f" 20h Zug: {len(zug_issues)} Issues")
print(f" Abrechnung/AC Trasse: {len(abr_issues)} Issues")
@@ -0,0 +1,149 @@
<#
.SYNOPSIS
Exportiert Jira-Daten aller pathOS Team-Boards
#>
$JiraUrl = "https://arija.jaas.service.deutschebahn.com"
$ApiBase = "$JiraUrl/rest"
$OutputDir = "project-audit\data\jira-teams"
$TeamProjects = @(
@{ key = "O2C404"; name = "Team 404 (Portal)" }
@{ key = "O2CCIB"; name = "Team CIB (Prozesse/Backend)" }
@{ key = "O2CZERO"; name = "Team Zero (TAF/TAP)" }
@{ key = "O2COS"; name = "OPs Squad (Infrastruktur)" }
@{ key = "O2CSYS"; name = "STeam/System (ehem.)" }
@{ key = "O2CQST"; name = "QA/Test" }
@{ key = "O2CDEVOPS"; name = "DevOps" }
)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "============================================================"
Write-Host " pathOS Team-Boards Jira Export"
Write-Host "============================================================"
$Token = Read-Host "Jira PAT"
$Headers = @{ "Authorization" = "Bearer $Token"; "Content-Type" = "application/json" }
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
function Invoke-JiraApi($Uri) {
try {
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
return ($R.Content | ConvertFrom-Json)
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
return $null
}
}
$Summary = @()
foreach ($Team in $TeamProjects) {
$Key = $Team.key
$Name = $Team.name
Write-Host ""
Write-Host ">> $Name ($Key)" -ForegroundColor Yellow
# Projekt-Info
$Proj = Invoke-JiraApi "$ApiBase/api/2/project/$Key"
if ($null -eq $Proj) { Write-Host " Nicht gefunden oder kein Zugriff" -ForegroundColor Red; continue }
Write-Host " Projekt: $($Proj.name)"
# Issues (letzte 90 Tage)
$Jql = "project = $Key AND updated >= -90d ORDER BY updated DESC"
$Issues = @()
$Start = 0
do {
$Uri = "$ApiBase/api/2/search?jql=$([Uri]::EscapeDataString($Jql))&startAt=$Start&maxResults=100&fields=summary,status,issuetype,assignee,labels,components,fixVersions,priority,created,updated,sprint"
$Result = Invoke-JiraApi $Uri
if ($null -eq $Result) { break }
$Issues += $Result.issues
$Start += 100
} while ($Issues.Count -lt $Result.total -and $Issues.Count -lt 500)
Write-Host " $($Issues.Count) Issues (90 Tage)"
# Analyse
$ByType = $Issues | Group-Object { $_.fields.issuetype.name }
$ByStatus = $Issues | Group-Object { $_.fields.status.name }
$Done = ($Issues | Where-Object { $_.fields.status.name -eq "Fertig" -or $_.fields.status.name -match "Done|Closed|Resolved" }).Count
$InProgress = ($Issues | Where-Object { $_.fields.status.name -match "Implementation|In Bearbeitung|In Progress|Review|Validierung" }).Count
$Open = $Issues.Count - $Done - $InProgress
$TeamInfo = [PSCustomObject]@{
Team = $Name
Key = $Key
Total = $Issues.Count
Done = $Done
InProgress = $InProgress
Open = $Open
Features = ($ByType | Where-Object { $_.Name -eq "Feature" }).Count
Enabler = ($ByType | Where-Object { $_.Name -eq "Enabler" }).Count
Bugs = ($ByType | Where-Object { $_.Name -match "Bug|Fehler" }).Count
Tasks = ($ByType | Where-Object { $_.Name -eq "Aufgabe" }).Count
}
$Summary += $TeamInfo
Write-Host " Fertig: $Done | In Arbeit: $InProgress | Offen: $Open"
# Issues als CSV speichern
$CsvData = $Issues | ForEach-Object {
[PSCustomObject]@{
Key = $_.key
Typ = $_.fields.issuetype.name
Status = $_.fields.status.name
Prioritaet = if ($_.fields.priority) { $_.fields.priority.name } else { "" }
Zusammenfassung = $_.fields.summary
Assignee = if ($_.fields.assignee) { $_.fields.assignee.displayName } else { "" }
Labels = if ($_.fields.labels) { $_.fields.labels -join ", " } else { "" }
Erstellt = $_.fields.created
Aktualisiert = $_.fields.updated
}
}
$CsvData | Export-Csv "$OutputDir\$Key-issues.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
$Issues | ConvertTo-Json -Depth 10 | Out-File "$OutputDir\$Key-issues.json" -Encoding UTF8
}
# Zusammenfassung
Write-Host ""
Write-Host ">> Zusammenfassung" -ForegroundColor Yellow
$Summary | Format-Table -AutoSize
$Md = @()
$Md += "# Jira Team-Boards Analyse pathOS"
$Md += ""
$Md += "> Exportiert: $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
$Md += ""
$Md += "| Team | Key | Issues (90d) | Fertig | In Arbeit | Offen | Features | Enabler | Bugs | Tasks |"
$Md += "|------|-----|-------------|--------|-----------|-------|----------|---------|------|-------|"
foreach ($T in $Summary) {
$Md += "| $($T.Team) | $($T.Key) | $($T.Total) | $($T.Done) | $($T.InProgress) | $($T.Open) | $($T.Features) | $($T.Enabler) | $($T.Bugs) | $($T.Tasks) |"
}
$Md += ""
# Assignee-Analyse (wer arbeitet wo)
$Md += "## Assignees pro Team"
foreach ($Team in $TeamProjects) {
$Key = $Team.key
$JsonPath = "$OutputDir\$Key-issues.json"
if (Test-Path $JsonPath) {
$Issues = Get-Content $JsonPath -Raw | ConvertFrom-Json
$Assignees = $Issues | Where-Object { $_.fields.assignee } | Group-Object { $_.fields.assignee.displayName } | Sort-Object Count -Descending
if ($Assignees.Count -gt 0) {
$Md += ""
$Md += "### $($Team.name) ($Key)"
foreach ($A in $Assignees) {
$Md += "- $($A.Name): $($A.Count) Issues"
}
}
}
}
$Md -join "`n" | Out-File "$OutputDir\team-summary.md" -Encoding UTF8
$Summary | Export-Csv "$OutputDir\team-summary.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " FERTIG! Ergebnisse in $OutputDir" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
@@ -0,0 +1,88 @@
<#
.SYNOPSIS
Test-Script fuer Jira API Queries - Debugging
#>
$JiraUrl = "https://arija.jaas.service.deutschebahn.com"
$ApiBase = "$JiraUrl/rest"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$Token = Read-Host "Jira PAT"
$Headers = @{ "Authorization" = "Bearer $Token"; "Content-Type" = "application/json" }
Write-Host "=== Test 1: Einfacher Name ==="
$Jql = "assignee = 'Ana Cvitkovic' ORDER BY created ASC"
$EncodedJql = [Uri]::EscapeDataString($Jql)
$Uri = "$ApiBase/api/2/search?jql=$EncodedJql&maxResults=1&fields=created,summary"
Write-Host " JQL: $Jql"
Write-Host " URI: $Uri"
try {
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
$Data = $R.Content | ConvertFrom-Json
Write-Host " OK: $($Data.total) Treffer" -ForegroundColor Green
if ($Data.issues.Count -gt 0) {
Write-Host " Erstes: $($Data.issues[0].key) - $($Data.issues[0].fields.created)"
}
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
if ($_.Exception.Response) {
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
$body = $reader.ReadToEnd()
Write-Host " Response: $($body.Substring(0, [Math]::Min(500, $body.Length)))" -ForegroundColor Yellow
}
}
Write-Host ""
Write-Host "=== Test 2: Name mit Bindestrich ==="
$Jql2 = "assignee = 'Dong-Won Han' ORDER BY created ASC"
$EncodedJql2 = [Uri]::EscapeDataString($Jql2)
$Uri2 = "$ApiBase/api/2/search?jql=$EncodedJql2&maxResults=1&fields=created,summary"
Write-Host " JQL: $Jql2"
try {
$R2 = Invoke-WebRequest -Uri $Uri2 -Headers $Headers -UseBasicParsing -ErrorAction Stop
$Data2 = $R2.Content | ConvertFrom-Json
Write-Host " OK: $($Data2.total) Treffer" -ForegroundColor Green
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
if ($_.Exception.Response) {
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
$body = $reader.ReadToEnd()
Write-Host " Response: $($body.Substring(0, [Math]::Min(500, $body.Length)))" -ForegroundColor Yellow
}
}
Write-Host ""
Write-Host "=== Test 3: Einfache Suche ohne Assignee ==="
$Jql3 = "project = O2C404 ORDER BY created ASC"
$EncodedJql3 = [Uri]::EscapeDataString($Jql3)
$Uri3 = "$ApiBase/api/2/search?jql=$EncodedJql3&maxResults=1&fields=created,summary,assignee"
Write-Host " JQL: $Jql3"
try {
$R3 = Invoke-WebRequest -Uri $Uri3 -Headers $Headers -UseBasicParsing -ErrorAction Stop
$Data3 = $R3.Content | ConvertFrom-Json
Write-Host " OK: $($Data3.total) Treffer" -ForegroundColor Green
if ($Data3.issues.Count -gt 0) {
$Assignee = if ($Data3.issues[0].fields.assignee) { $Data3.issues[0].fields.assignee.displayName } else { "unassigned" }
Write-Host " Erstes: $($Data3.issues[0].key) - Assignee: $Assignee"
}
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
if ($_.Exception.Response) {
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
$body = $reader.ReadToEnd()
Write-Host " Response: $($body.Substring(0, [Math]::Min(500, $body.Length)))" -ForegroundColor Yellow
}
}
Write-Host ""
Write-Host "=== Test 4: currentUser ==="
$Jql4 = "assignee = currentUser() ORDER BY created ASC"
$Uri4 = "$ApiBase/api/2/search?jql=$([Uri]::EscapeDataString($Jql4))&maxResults=1&fields=created,summary"
try {
$R4 = Invoke-WebRequest -Uri $Uri4 -Headers $Headers -UseBasicParsing -ErrorAction Stop
$Data4 = $R4.Content | ConvertFrom-Json
Write-Host " OK: $($Data4.total) Treffer" -ForegroundColor Green
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
}
@@ -0,0 +1,125 @@
<#
.SYNOPSIS
Velocity-Analyse ueber 12 Monate fuer alle pathOS Teams
#>
$JiraUrl = "https://arija.jaas.service.deutschebahn.com"
$ApiBase = "$JiraUrl/rest"
$OutputDir = "project-audit\data\jira-velocity"
$TeamProjects = @(
@{ key = "O2C404"; name = "Team 404" }
@{ key = "O2CCIB"; name = "Team CIB" }
@{ key = "O2CZERO"; name = "Team Zero" }
@{ key = "O2COS"; name = "OPs Squad" }
@{ key = "O2CDEVOPS"; name = "DevOps" }
)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "============================================================"
Write-Host " pathOS Velocity-Analyse (12 Monate)"
Write-Host "============================================================"
$Token = Read-Host "Jira PAT"
$Headers = @{ "Authorization" = "Bearer $Token"; "Content-Type" = "application/json" }
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
function Invoke-JiraApi($Uri) {
try {
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
return ($R.Content | ConvertFrom-Json)
} catch { return $null }
}
$AllResults = @()
foreach ($Team in $TeamProjects) {
$Key = $Team.key
$Name = $Team.name
Write-Host ""
Write-Host ">> $Name ($Key)" -ForegroundColor Yellow
# Alle resolved/done Issues der letzten 12 Monate
$Jql = "project = $Key AND status in (Fertig, Done, Closed, Resolved) AND resolved >= -365d ORDER BY resolved ASC"
$Issues = @()
$Start = 0
do {
$Uri = "$ApiBase/api/2/search?jql=$([Uri]::EscapeDataString($Jql))&startAt=$Start&maxResults=100&fields=summary,status,issuetype,resolutiondate,created"
$Result = Invoke-JiraApi $Uri
if ($null -eq $Result) { break }
$Issues += $Result.issues
$Start += 100
Write-Host " $($Issues.Count) / $($Result.total)..." -NoNewline
} while ($Issues.Count -lt $Result.total -and $Issues.Count -lt 3000)
Write-Host ""
Write-Host " $($Issues.Count) resolved Issues (12 Monate)" -ForegroundColor Green
# Nach Monat gruppieren
$ByMonth = @{}
$BugsByMonth = @{}
$EnablerByMonth = @{}
foreach ($I in $Issues) {
$ResDate = $I.fields.resolutiondate
if (-not $ResDate) { $ResDate = $I.fields.created }
if ($ResDate) {
try {
$Date = [DateTime]::Parse($ResDate)
$MK = $Date.ToString("yyyy-MM")
$ByMonth[$MK] = ($ByMonth[$MK]) + 1
$Type = $I.fields.issuetype.name
if ($Type -eq "Bug" -or $Type -eq "Fehler") { $BugsByMonth[$MK] = ($BugsByMonth[$MK]) + 1 }
if ($Type -eq "Enabler") { $EnablerByMonth[$MK] = ($EnablerByMonth[$MK]) + 1 }
} catch {}
}
}
# Ergebnis sammeln
foreach ($MK in ($ByMonth.Keys | Sort-Object)) {
$Total = $ByMonth[$MK]
$Bugs = if ($BugsByMonth[$MK]) { $BugsByMonth[$MK] } else { 0 }
$Enablers = if ($EnablerByMonth[$MK]) { $EnablerByMonth[$MK] } else { 0 }
$AllResults += [PSCustomObject]@{
Team = $Name
Key = $Key
Month = $MK
Total = $Total
Bugs = $Bugs
Enabler = $Enablers
BugRate = if ($Total -gt 0) { [Math]::Round(($Bugs / $Total) * 100, 0) } else { 0 }
}
}
}
# CSV speichern
$AllResults | Export-Csv "$OutputDir\velocity-12m.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
# Markdown
$Md = @()
$Md += "# Velocity-Analyse pathOS (12 Monate)"
$Md += ""
$Md += "> Exportiert: $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
$Md += ""
$Md += "| Team | Monat | Gesamt | Bugs | Enabler | Bug% |"
$Md += "|------|-------|--------|------|---------|------|"
foreach ($R in $AllResults) {
$Md += "| $($R.Team) | $($R.Month) | $($R.Total) | $($R.Bugs) | $($R.Enabler) | $($R.BugRate)% |"
}
$Md -join "`n" | Out-File "$OutputDir\velocity-12m.md" -Encoding UTF8
# Zusammenfassung pro Team
Write-Host ""
Write-Host "=== Zusammenfassung ==="
foreach ($Team in $TeamProjects) {
$TeamData = $AllResults | Where-Object { $_.Key -eq $Team.key }
$TotalAll = ($TeamData | Measure-Object -Property Total -Sum).Sum
$TotalBugs = ($TeamData | Measure-Object -Property Bugs -Sum).Sum
$AvgMonth = if ($TeamData.Count -gt 0) { [Math]::Round($TotalAll / $TeamData.Count, 0) } else { 0 }
Write-Host " $($Team.name): $TotalAll Issues (12M), Avg $AvgMonth/Monat, $TotalBugs Bugs ($([Math]::Round($TotalBugs/$TotalAll*100,0))%)"
}
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " FERTIG! Ergebnisse in $OutputDir" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
@@ -0,0 +1,10 @@
<#
.SYNOPSIS
Fuehrt Push und Reorganisation nacheinander aus
#>
Write-Host "=== Schritt 1: Push ===" -ForegroundColor Cyan
powershell -ExecutionPolicy Bypass -File ".\project-audit\scripts\confluence-push.ps1"
Write-Host ""
Write-Host "=== Schritt 2: Reorganisation ===" -ForegroundColor Cyan
powershell -ExecutionPolicy Bypass -File ".\project-audit\scripts\confluence-reorganize.ps1"
@@ -0,0 +1,27 @@
"""Stellt den Default Branch auf master um."""
import urllib.request, json, ssl
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
TOKEN = secrets.get("GITLAB_TOKEN_AUDIT", "")
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
url = "https://git.tech.rz.db.de/api/v4/projects/AndreKnie%2Fproject-audit"
data = json.dumps({"default_branch": "master"}).encode("utf-8")
req = urllib.request.Request(url, data=data, method="PUT")
req.add_header("PRIVATE-TOKEN", TOKEN)
req.add_header("Content-Type", "application/json")
try:
resp = urllib.request.urlopen(req, context=ctx, timeout=15)
result = json.loads(resp.read().decode("utf-8"))
print(f"Default Branch jetzt: {result.get('default_branch', '?')}")
print("Erfolgreich umgestellt!")
except Exception as e:
print(f"Fehler: {e}")
@@ -0,0 +1,120 @@
"""
SonarQube-Daten analysieren: Coverage und Smells pro Service/Team.
Filtert auf master/main Branches und ordnet Services den Teams zu.
"""
import csv
from collections import defaultdict
CSV_PATH = "project-audit/data/sonarqube/sonarqube-metrics.csv"
# Team-Zuordnung der Services
TEAM_MAP = {
"portal-ui": "404",
"portal-middleware": "404",
"portal-mw": "404",
"steuerung-vertrieb": "CIB",
"sv-": "CIB",
"auftrags-verwaltung": "CIB",
"av-": "CIB",
"auftrag-service": "CIB",
"archivierungsservice": "CIB",
"common-interface": "Zero",
"ci-": "Zero",
"tadef": "Zero",
"stammdaten": "Zero",
"stb-": "Zero",
"acat": "Zero",
"vertragsdaten": "Zero",
"vdv": "Zero",
"ifp": "CIB",
"kundendaten": "CIB",
"kdv": "CIB",
"rabatt": "CIB",
"core-component": "Shared",
"signature": "Shared",
"logging": "Shared",
}
def get_team(name):
name_lower = name.lower()
for pattern, team in TEAM_MAP.items():
if pattern in name_lower:
return team
return "Andere"
# Daten laden (nur master/main)
data = []
with open(CSV_PATH, "r", encoding="utf-8") as f:
reader = csv.DictReader(f, delimiter=";", quotechar='"')
for row in reader:
name = row.get("Name", "")
if name.endswith("-master") or name.endswith("-main"):
lines = int(row.get("Lines", "0") or "0")
if lines > 0: # Nur Projekte mit Code
data.append({
"name": name.replace("-master", "").replace("-main", ""),
"lines": lines,
"bugs": int(row.get("Bugs", "0") or "0"),
"smells": int(row.get("CodeSmells", "0") or "0"),
"coverage": float(row.get("Coverage", "0") or "0"),
"duplications": float(row.get("Duplications", "0") or "0"),
"qgate": row.get("QualityGate", "?"),
"team": get_team(name),
})
# Sortiert nach Lines
data.sort(key=lambda x: x["lines"], reverse=True)
print(f"=== SonarQube Analyse (nur master/main, {len(data)} Projekte mit Code) ===")
print()
# Top 20 nach Groesse
print("--- Top 20 Projekte nach Groesse ---")
print(f"{'Projekt':<40} {'Lines':>7} {'Bugs':>5} {'Smells':>7} {'Cov%':>6} {'QGate':>6} {'Team':<8}")
print("-" * 85)
for row in data[:20]:
cov_str = f"{row['coverage']:.1f}" if row['coverage'] > 0 else "-"
print(f"{row['name']:<40} {row['lines']:>7} {row['bugs']:>5} {row['smells']:>7} {cov_str:>6} {row['qgate']:>6} {row['team']:<8}")
# Team-Aggregation
print()
print("--- Aggregation pro Team ---")
teams = defaultdict(lambda: {"lines": 0, "bugs": 0, "smells": 0, "cov_weighted": 0, "projects": 0, "qgate_ok": 0, "qgate_err": 0})
for row in data:
t = teams[row["team"]]
t["lines"] += row["lines"]
t["bugs"] += row["bugs"]
t["smells"] += row["smells"]
t["cov_weighted"] += row["coverage"] * row["lines"]
t["projects"] += 1
if row["qgate"] == "OK":
t["qgate_ok"] += 1
else:
t["qgate_err"] += 1
print(f"{'Team':<10} {'Projekte':>8} {'Lines':>8} {'Bugs':>5} {'Smells':>7} {'Smells/1K':>9} {'Cov%':>6} {'QGate OK':>8}")
print("-" * 75)
for team in ["404", "CIB", "Zero", "Shared", "Andere"]:
if team in teams:
t = teams[team]
smells_per_k = (t["smells"] / t["lines"] * 1000) if t["lines"] > 0 else 0
avg_cov = (t["cov_weighted"] / t["lines"]) if t["lines"] > 0 else 0
qgate_str = f"{t['qgate_ok']}/{t['projects']}"
print(f"{team:<10} {t['projects']:>8} {t['lines']:>8} {t['bugs']:>5} {t['smells']:>7} {smells_per_k:>9.1f} {avg_cov:>6.1f} {qgate_str:>8}")
# Projekte ohne Coverage
print()
print("--- Projekte OHNE Coverage (0% oder keine Daten) ---")
no_cov = [r for r in data if r["coverage"] == 0 and r["lines"] > 500]
no_cov.sort(key=lambda x: x["lines"], reverse=True)
for row in no_cov:
print(f" {row['name']:<40} {row['lines']:>7} Lines {row['team']:<8}")
# Quality Gate Failures
print()
print("--- Quality Gate FAILED (ERROR) ---")
failed = [r for r in data if r["qgate"] == "ERROR"]
failed.sort(key=lambda x: x["lines"], reverse=True)
for row in failed[:15]:
cov_str = f"{row['coverage']:.1f}%" if row['coverage'] > 0 else "0%"
print(f" {row['name']:<40} {row['lines']:>7} Lines Bugs={row['bugs']} Smells={row['smells']} Cov={cov_str} {row['team']}")
@@ -0,0 +1,158 @@
<#
.SYNOPSIS
Exportiert SonarQube Metriken fuer alle pathOS Projekte
#>
$SonarUrl = "https://sonarqube.build.bsz-dev.cnp-test.comp.db.de"
$OutputDir = "project-audit\data\sonarqube"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "============================================================"
Write-Host " SonarQube Export - pathOS"
Write-Host " URL: $SonarUrl"
Write-Host "============================================================"
Write-Host ""
Write-Host " Token erstellen: $SonarUrl/account/security"
Write-Host ""
$SonarToken = Read-Host "SonarQube Token"
$SonarAuth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${SonarToken}:"))
$SonarHeaders = @{ "Authorization" = "Basic $SonarAuth" }
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
# Test Authentifizierung
Write-Host ">> Teste Authentifizierung..." -NoNewline
try {
$AuthTest = Invoke-WebRequest -Uri "$SonarUrl/api/authentication/validate" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
$AuthData = $AuthTest.Content | ConvertFrom-Json
if ($AuthData.valid) {
Write-Host " OK (authentifiziert)" -ForegroundColor Green
} else {
Write-Host " Token ungueltig!" -ForegroundColor Red
Write-Host " Bitte Token unter $SonarUrl/account/security neu generieren."
Write-Host " Token-Typ: 'User Token' (nicht 'Project Token')"
exit 1
}
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " Versuche ohne Auth (falls anonymer Zugriff erlaubt)..."
$SonarHeaders = @{}
}
# Projekte laden - verschiedene Endpunkte probieren
Write-Host ">> Lade Projekte..." -NoNewline
$Projects = @()
$Page = 1
# Versuch 1: projects/search (braucht Admin oder Browse-Rechte)
try {
$R = Invoke-WebRequest -Uri "$SonarUrl/api/projects/search?ps=100&p=1" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
$Data = $R.Content | ConvertFrom-Json
$Projects += $Data.components
Write-Host " OK (projects/search)" -ForegroundColor Green
# Rest laden
while ($Projects.Count -lt $Data.paging.total) {
$Page++
$R = Invoke-WebRequest -Uri "$SonarUrl/api/projects/search?ps=100&p=$Page" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
$Data = $R.Content | ConvertFrom-Json
$Projects += $Data.components
}
} catch {
Write-Host " 403 auf projects/search, versuche components/search..." -ForegroundColor Yellow
# Versuch 2: components/search_projects
try {
$R = Invoke-WebRequest -Uri "$SonarUrl/api/components/search_projects?ps=100&p=1" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
$Data = $R.Content | ConvertFrom-Json
$Projects += $Data.components
Write-Host " OK (components/search_projects)" -ForegroundColor Green
while ($Projects.Count -lt $Data.paging.total) {
$Page++
$R = Invoke-WebRequest -Uri "$SonarUrl/api/components/search_projects?ps=100&p=$Page" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
$Data = $R.Content | ConvertFrom-Json
$Projects += $Data.components
}
} catch {
Write-Host " Auch 403. Versuche components/search..." -ForegroundColor Yellow
# Versuch 3: components/search mit Qualifier TRK
try {
$R = Invoke-WebRequest -Uri "$SonarUrl/api/components/search?qualifiers=TRK&ps=100&p=1" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
$Data = $R.Content | ConvertFrom-Json
$Projects += $Data.components
Write-Host " OK (components/search)" -ForegroundColor Green
while ($Projects.Count -lt $Data.paging.total) {
$Page++
$R = Invoke-WebRequest -Uri "$SonarUrl/api/components/search?qualifiers=TRK&ps=100&p=$Page" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
$Data = $R.Content | ConvertFrom-Json
$Projects += $Data.components
}
} catch {
Write-Host ""
Write-Host " Alle Endpunkte geben 403." -ForegroundColor Red
Write-Host " Dein Account ist authentifiziert aber hat keine Projekt-Browse-Rechte ueber die API." -ForegroundColor Red
Write-Host " Loesung: Bitte im OPs-Team anfragen, dich zur SonarQube-Gruppe 'bestellsystem1' hinzuzufuegen." -ForegroundColor Yellow
Write-Host " Alternativ: Bekannte Projekt-Keys direkt abfragen (siehe unten)." -ForegroundColor Yellow
# Fallback: Bekannte Projekte direkt abfragen
Write-Host ""
Write-Host ">> Fallback: Bekannte Projekte direkt abfragen..." -ForegroundColor Yellow
$KnownKeys = @(
"steuerung-vertrieb", "auftrags-verwaltung-trasse", "portal-middleware", "portal-ui",
"common-interface", "stammdaten-bereitstellung", "kundendaten-bereitstellung",
"ifp-connector", "taftap-tdm-konverter", "vertragsdaten-verteiler",
"archivierungsservice", "rabattnummern-bereitstellung", "tbv-absicherung-konverter"
)
foreach ($Key in $KnownKeys) {
$Projects += [PSCustomObject]@{ key = $Key; name = $Key }
}
Write-Host " $($Projects.Count) bekannte Projekte" -ForegroundColor Green
}
}
}
Write-Host " $($Projects.Count) Projekte" -ForegroundColor Green
# Metriken pro Projekt
Write-Host ">> Lade Metriken..."
$Metrics = "ncloc,bugs,vulnerabilities,code_smells,coverage,duplicated_lines_density,sqale_rating,reliability_rating,security_rating,alert_status"
$Results = @()
foreach ($P in $Projects) {
Write-Host " $($P.key)..." -NoNewline
try {
$R = Invoke-WebRequest -Uri "$SonarUrl/api/measures/component?component=$($P.key)&metricKeys=$Metrics" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
$MData = ($R.Content | ConvertFrom-Json).component.measures
$Results += [PSCustomObject]@{
Project = $P.key
Name = $P.name
Lines = ($MData | Where-Object { $_.metric -eq "ncloc" }).value
Bugs = ($MData | Where-Object { $_.metric -eq "bugs" }).value
Vulnerabilities = ($MData | Where-Object { $_.metric -eq "vulnerabilities" }).value
CodeSmells = ($MData | Where-Object { $_.metric -eq "code_smells" }).value
Coverage = ($MData | Where-Object { $_.metric -eq "coverage" }).value
Duplications = ($MData | Where-Object { $_.metric -eq "duplicated_lines_density" }).value
Maintainability = ($MData | Where-Object { $_.metric -eq "sqale_rating" }).value
Reliability = ($MData | Where-Object { $_.metric -eq "reliability_rating" }).value
Security = ($MData | Where-Object { $_.metric -eq "security_rating" }).value
QualityGate = ($MData | Where-Object { $_.metric -eq "alert_status" }).value
}
Write-Host " OK" -ForegroundColor Green
} catch { Write-Host " Fehler" -ForegroundColor Yellow }
}
# Speichern
$Results | Export-Csv "$OutputDir\sonarqube-metrics.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
$Results | ConvertTo-Json -Depth 5 | Out-File "$OutputDir\sonarqube-metrics.json" -Encoding UTF8
$Md = @("# SonarQube Metriken pathOS", "", "| Projekt | Lines | Bugs | Vulns | Smells | Coverage | Dupl% | QGate |")
$Md += "|---------|-------|------|-------|--------|----------|-------|-------|"
foreach ($R in ($Results | Sort-Object { if ($_.Lines) { [int]$_.Lines } else { 0 } } -Descending)) {
$Md += "| $($R.Name) | $($R.Lines) | $($R.Bugs) | $($R.Vulnerabilities) | $($R.CodeSmells) | $($R.Coverage)% | $($R.Duplications)% | $($R.QualityGate) |"
}
$Md -join "`n" | Out-File "$OutputDir\sonarqube-metrics.md" -Encoding UTF8
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " FERTIG! $($Results.Count) Projekte" -ForegroundColor Green
Write-Host " Ergebnisse in $OutputDir" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
@@ -0,0 +1,178 @@
<#
.SYNOPSIS
Schritt 1: SonarQube URL aus GitLab Deployment-Repo finden
Schritt 2: SonarQube Metriken exportieren (braucht SonarQube Token)
#>
$GitLabUrl = "https://git.tech.rz.db.de"
$GitLabApiBase = "$GitLabUrl/api/v4"
$OutputDir = "project-audit\data\sonarqube"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "============================================================"
Write-Host " SonarQube Daten fuer pathOS"
Write-Host "============================================================"
# === Schritt 1: URL aus GitLab finden ===
Write-Host ""
Write-Host ">> Schritt 1: SonarQube URL aus GitLab suchen..." -ForegroundColor Yellow
$GitLabToken = Read-Host "GitLab Token (read_api)"
$GitLabHeaders = @{ "PRIVATE-TOKEN" = $GitLabToken }
# deployment-sonarqube Repo durchsuchen
$RepoPath = [Uri]::EscapeDataString("bestellsystem1/infra/deployment-sonarqube")
$SonarUrl = $null
foreach ($File in @("values.yaml", "helmfile.yaml", "Chart.yaml", "values-sonarqube.yaml")) {
foreach ($Branch in @("main", "master")) {
$Uri = "$GitLabApiBase/projects/$RepoPath/repository/files/$([Uri]::EscapeDataString($File))/raw?ref=$Branch"
try {
$Resp = Invoke-WebRequest -Uri $Uri -Headers $GitLabHeaders -UseBasicParsing -ErrorAction Stop
$Content = $Resp.Content
Write-Host " Gefunden: $File ($Branch)" -ForegroundColor Green
# URL suchen
if ($Content -match "(https?://sonar[^\s""']+)") {
$SonarUrl = $Matches[1]
Write-Host " SonarQube URL: $SonarUrl" -ForegroundColor Green
}
if ($Content -match "host:\s*(.+)") {
Write-Host " Host: $($Matches[1].Trim())"
}
if ($Content -match "ingress.*host.*:\s*(.+)") {
Write-Host " Ingress Host: $($Matches[1].Trim())"
}
# Datei speichern
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
$Content | Out-File "$OutputDir\deployment-$File" -Encoding UTF8
} catch { continue }
}
}
# Auch in der Repo-Beschreibung suchen
try {
$RepoInfo = Invoke-WebRequest -Uri "$GitLabApiBase/projects/$RepoPath" -Headers $GitLabHeaders -UseBasicParsing -ErrorAction Stop
$RepoData = $RepoInfo.Content | ConvertFrom-Json
Write-Host " Repo-Beschreibung: $($RepoData.description)"
} catch {}
# Bekannte CNP-URL-Muster probieren
Write-Host ""
Write-Host ">> Probiere bekannte URL-Muster..." -ForegroundColor Yellow
$Candidates = @(
"https://sonarqube.build.bsz-dev.cnp-test.comp.db.de"
"https://sonarqube.bsz-dev.cnp-test.comp.db.de"
"https://sonarqube.bsz-infra.cnp-test.comp.db.de"
)
foreach ($Url in $Candidates) {
Write-Host " Teste $Url..." -NoNewline
try {
$R = Invoke-WebRequest -Uri "$Url/api/system/status" -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop
Write-Host " ERREICHBAR!" -ForegroundColor Green
$SonarUrl = $Url
break
} catch {
Write-Host " nicht erreichbar" -ForegroundColor DarkGray
}
}
if (-not $SonarUrl) {
Write-Host ""
Write-Host "SonarQube URL nicht automatisch gefunden." -ForegroundColor Yellow
Write-Host "Bitte manuell eingeben (oder Enter zum Ueberspringen):"
$SonarUrl = Read-Host "SonarQube URL"
if (-not $SonarUrl) {
Write-Host "Uebersprungen. Bitte URL beim Team erfragen." -ForegroundColor Yellow
exit 0
}
}
# === Schritt 2: SonarQube Metriken ziehen ===
Write-Host ""
Write-Host ">> Schritt 2: SonarQube Metriken exportieren..." -ForegroundColor Yellow
Write-Host " URL: $SonarUrl"
Write-Host ""
Write-Host " Du brauchst einen SonarQube Token."
Write-Host " Erstellen unter: $SonarUrl/account/security"
Write-Host " (Login mit GitLab SSO, dann My Account > Security > Generate Token)"
Write-Host ""
$SonarToken = Read-Host "SonarQube Token (oder Enter zum Ueberspringen)"
if (-not $SonarToken) {
Write-Host "Uebersprungen." -ForegroundColor Yellow
exit 0
}
$SonarAuth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${SonarToken}:"))
$SonarHeaders = @{ "Authorization" = "Basic $SonarAuth" }
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
# Projekte auflisten
Write-Host " Lade Projekte..." -NoNewline
$Projects = @()
$Page = 1
do {
try {
$R = Invoke-WebRequest -Uri "$SonarUrl/api/projects/search?ps=100&p=$Page" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
$Data = $R.Content | ConvertFrom-Json
$Projects += $Data.components
$Page++
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
break
}
} while ($Projects.Count -lt $Data.paging.total)
Write-Host " $($Projects.Count) Projekte" -ForegroundColor Green
# Metriken pro Projekt
Write-Host " Lade Metriken..."
$Metrics = "ncloc,bugs,vulnerabilities,code_smells,coverage,duplicated_lines_density,sqale_rating,reliability_rating,security_rating,alert_status"
$Results = @()
foreach ($P in $Projects) {
Write-Host " $($P.key)..." -NoNewline
try {
$R = Invoke-WebRequest -Uri "$SonarUrl/api/measures/component?component=$($P.key)&metricKeys=$Metrics" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
$MData = ($R.Content | ConvertFrom-Json).component.measures
$Row = [PSCustomObject]@{
Project = $P.key
Name = $P.name
Lines = ($MData | Where-Object { $_.metric -eq "ncloc" }).value
Bugs = ($MData | Where-Object { $_.metric -eq "bugs" }).value
Vulnerabilities = ($MData | Where-Object { $_.metric -eq "vulnerabilities" }).value
CodeSmells = ($MData | Where-Object { $_.metric -eq "code_smells" }).value
Coverage = ($MData | Where-Object { $_.metric -eq "coverage" }).value
Duplications = ($MData | Where-Object { $_.metric -eq "duplicated_lines_density" }).value
Maintainability = ($MData | Where-Object { $_.metric -eq "sqale_rating" }).value
Reliability = ($MData | Where-Object { $_.metric -eq "reliability_rating" }).value
Security = ($MData | Where-Object { $_.metric -eq "security_rating" }).value
QualityGate = ($MData | Where-Object { $_.metric -eq "alert_status" }).value
}
$Results += $Row
Write-Host " OK" -ForegroundColor Green
} catch {
Write-Host " Fehler" -ForegroundColor Yellow
}
}
# Speichern
$Results | Export-Csv "$OutputDir\sonarqube-metrics.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
$Results | ConvertTo-Json -Depth 5 | Out-File "$OutputDir\sonarqube-metrics.json" -Encoding UTF8
# Markdown
$Md = @("# SonarQube Metriken pathOS", "", "| Projekt | Lines | Bugs | Vulns | Smells | Coverage | Dupl% | QGate |")
$Md += "|---------|-------|------|-------|--------|----------|-------|-------|"
foreach ($R in ($Results | Sort-Object { [int]$_.Lines } -Descending)) {
$Md += "| $($R.Name) | $($R.Lines) | $($R.Bugs) | $($R.Vulnerabilities) | $($R.CodeSmells) | $($R.Coverage)% | $($R.Duplications)% | $($R.QualityGate) |"
}
$Md -join "`n" | Out-File "$OutputDir\sonarqube-metrics.md" -Encoding UTF8
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " FERTIG! $($Results.Count) Projekte analysiert" -ForegroundColor Green
Write-Host " Ergebnisse in $OutputDir" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
@@ -0,0 +1,120 @@
<#
.SYNOPSIS
Exportiert Top Code Smells pro Komponente aus SonarQube
#>
$SonarUrl = "https://sonarqube.build.bsz-dev.cnp-test.comp.db.de"
$OutputDir = "project-audit\data\sonarqube"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "============================================================"
Write-Host " SonarQube Smells Detail-Analyse"
Write-Host "============================================================"
$SonarToken = Read-Host "SonarQube Token"
$SonarAuth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${SonarToken}:"))
$SonarHeaders = @{ "Authorization" = "Basic $SonarAuth" }
# Kern-Projekte (Master/Main Branch Keys)
$Projects = @(
@{ key = "steuerung-vertrieb-master"; name = "Steuerung Vertrieb"; team = "CIB" }
@{ key = "portal-ui-master"; name = "Portal UI"; team = "404" }
@{ key = "portal-middleware-master"; name = "Portal Middleware"; team = "404" }
@{ key = "auftrags-verwaltung-trasse-master"; name = "Auftrags-Verwaltung"; team = "CIB" }
@{ key = "common-interface-master"; name = "Common Interface"; team = "Zero" }
@{ key = "taftap-tdm-konverter-main"; name = "TAF/TAP TDM Konverter"; team = "Zero" }
@{ key = "archivierungsservice-master"; name = "Archivierungsservice"; team = "CIB" }
@{ key = "stammdaten-bereitstellung-master"; name = "Stammdaten-Bereitstellung"; team = "Zero" }
@{ key = "tadef-connector-main"; name = "TADEF Connector"; team = "Zero" }
@{ key = "camunda-backup-wizard-main"; name = "Camunda Backup Wizard"; team = "CIB" }
)
$AllSmells = @()
foreach ($P in $Projects) {
Write-Host ""
Write-Host ">> $($P.name) ($($P.team))..." -ForegroundColor Yellow
# Issues mit Typ CODE_SMELL laden
$Issues = @()
$Page = 1
do {
try {
$Uri = "$SonarUrl/api/issues/search?componentKeys=$($P.key)&types=CODE_SMELL&statuses=OPEN,CONFIRMED&ps=100&p=$Page&facets=rules"
$R = Invoke-WebRequest -Uri $Uri -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
$Data = $R.Content | ConvertFrom-Json
$Issues += $Data.issues
if ($Page -eq 1 -and $Data.facets) {
# Rule-Facetten extrahieren (Top Smells nach Haeufigkeit)
$RuleFacet = $Data.facets | Where-Object { $_.property -eq "rules" }
if ($RuleFacet) {
Write-Host " $($Data.total) Smells, Top Rules:"
$TopRules = $RuleFacet.values | Sort-Object count -Descending | Select-Object -First 15
foreach ($Rule in $TopRules) {
$AllSmells += [PSCustomObject]@{
Project = $P.name
Team = $P.team
Rule = $Rule.val
Count = $Rule.count
}
Write-Host " $($Rule.val): $($Rule.count)"
}
}
}
$Page++
} catch {
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
break
}
} while ($Issues.Count -lt [Math]::Min($Data.total, 500) -and $Page -le 5)
}
# Speichern
$AllSmells | Export-Csv "$OutputDir\smells-by-rule.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
# Cross-Projekt Analyse: Gleiche Rules ueber Projekte
Write-Host ""
Write-Host "=== Cross-Projekt Analyse ===" -ForegroundColor Yellow
$ByRule = $AllSmells | Group-Object Rule
$CrossProject = $ByRule | Where-Object { $_.Count -gt 1 } | Sort-Object { ($_.Group | Measure-Object -Property Count -Sum).Sum } -Descending
Write-Host ""
Write-Host "Rules die in mehreren Projekten auftreten:"
foreach ($R in $CrossProject | Select-Object -First 20) {
$TotalCount = ($R.Group | Measure-Object -Property Count -Sum).Sum
$ProjectList = ($R.Group | ForEach-Object { "$($_.Project)($($_.Count))" }) -join ", "
Write-Host " $($R.Name): $TotalCount total [$ProjectList]"
}
# Markdown
$Md = @("# SonarQube Smells Detail-Analyse", "")
$Md += "## Top Smells pro Komponente"
$Md += ""
foreach ($P in $Projects) {
$PSmells = $AllSmells | Where-Object { $_.Project -eq $P.name } | Sort-Object Count -Descending
if ($PSmells.Count -eq 0) { continue }
$Md += "### $($P.name) ($($P.team))"
$Md += "| Rule | Count |"
$Md += "|------|-------|"
foreach ($S in $PSmells | Select-Object -First 10) {
$Md += "| $($S.Rule) | $($S.Count) |"
}
$Md += ""
}
$Md += "## Cross-Projekt: Gleiche Rules"
$Md += "| Rule | Total | Projekte |"
$Md += "|------|-------|----------|"
foreach ($R in $CrossProject | Select-Object -First 20) {
$TotalCount = ($R.Group | Measure-Object -Property Count -Sum).Sum
$ProjectList = ($R.Group | ForEach-Object { "$($_.Project)($($_.Count))" }) -join ", "
$Md += "| $($R.Name) | $TotalCount | $ProjectList |"
}
$Md -join "`n" | Out-File "$OutputDir\smells-detail.md" -Encoding UTF8
Write-Host ""
Write-Host "============================================================" -ForegroundColor Green
Write-Host " FERTIG! Ergebnisse in $OutputDir" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
+45
View File
@@ -0,0 +1,45 @@
"""Liest TrassenOrder User Research von SAB Confluence."""
import urllib.request, json, ssl, re
secrets = {}
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
secrets[k.strip()] = v.strip()
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
CONF_TOKEN = secrets.get("CONFLUENCE_SAB", "")
CONF_URL = "https://systelone-confluence.jaas.service.deutschebahn.com/rest/api"
PAGE_ID = "85858484"
print(f"Token: ...{CONF_TOKEN[-4:]}")
print(f"URL: {CONF_URL}/content/{PAGE_ID}")
print()
try:
req = urllib.request.Request(f"{CONF_URL}/content/{PAGE_ID}?expand=body.storage")
req.add_header("Authorization", f"Bearer {CONF_TOKEN}")
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
data = json.loads(resp.read().decode("utf-8"))
title = data.get("title", "?")
body = data["body"]["storage"]["value"]
print(f"Titel: {title}")
# HTML zu Text
text = re.sub(r'<[^>]+>', '\n', body)
text = re.sub(r'\n{3,}', '\n\n', text).strip()
print(f"Laenge: {len(text)} Zeichen")
print()
print(text[:5000])
# Speichern
with open("project-audit/data/trapo-user-research.txt", "w", encoding="utf-8") as f:
f.write(text)
print(f"\n\nGespeichert: project-audit/data/trapo-user-research.txt ({len(text)} Zeichen)")
except Exception as e:
print(f"Fehler: {e}")