From 38c9474a7e10444a4fbc2333899d007715964112 Mon Sep 17 00:00:00 2001
From: Samuel Goebel
Date: Mon, 4 May 2026 15:11:18 +0200
Subject: [PATCH 01/85] Added Algorithm, which returns a sorted list of recipes
of an matching Ingridients List
---
project/backend/Database.py | 53 +++++++++++++++++++++++++++++++++-
project/backend/Recipe.py | 25 ++++++++++------
project/backend/RecipeSucuk.py | 36 +++++++++++++++++++++++
3 files changed, 104 insertions(+), 10 deletions(-)
create mode 100644 project/backend/RecipeSucuk.py
diff --git a/project/backend/Database.py b/project/backend/Database.py
index 0d469cf..379d999 100644
--- a/project/backend/Database.py
+++ b/project/backend/Database.py
@@ -74,6 +74,7 @@ def initDB():
CREATE TABLE IF NOT EXISTS Recipe (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
+ description nvarchar(MAX),
vid INTEGER NOT NULL,
FOREIGN KEY (vid) REFERENCES Author (id) ON DELETE CASCADE
)
@@ -203,4 +204,54 @@ def cleanupExpiredTokens() -> None:
"""Löscht alle abgelaufenen Refresh Tokens."""
with getDB() as con:
cur = con.cursor()
- cur.execute("DELETE FROM RefreshToken WHERE expiresAt < datetime('now')")
\ No newline at end of file
+ cur.execute("DELETE FROM RefreshToken WHERE expiresAt < datetime('now')")
+
+def getRecipe(recipeID: int) -> dict | None:
+ """Gibt eine Liste aller Rezepte zurück."""
+ with getDB() as con:
+ cur = con.cursor()
+ cur.execute("""
+ SELECT name, description
+ FROM Recipe
+ WHERE id = ?
+ """, (recipeID,))
+ row = cur.fetchone()
+ return dict(row) if row else None
+
+def getAllRecipes()-> list[dict]:
+ with getDB() as con:
+ cur = con.cursor()
+ cur.execute("""
+ SELECT id, name, description
+ FROM Recipe
+
+ """)
+ rows = cur.fetchall()
+ return [dict(row) for row in rows]
+def getAllIngridientsForRecipe(id: int) -> list[dict]:
+ """Gibt eine Liste aller Zutaten zurück."""
+ with getDB() as con:
+ cur = con.cursor()
+ cur.execute("""
+ SELECT id, name, Exists_from.amount
+ FROM Ingridient
+ Inner Join Exists_from id = zid
+ Inner Join Recipe rid = id
+ Where id = ?
+ """, (id,))
+ rows = cur.fetchall()
+ return [dict(row) for row in rows]
+
+def getAllocatedRecipes(name: str)-> list[dict]:
+
+ with getDB() as con:
+ cur = con.cursor()
+ cur.execute("""
+ SELECT rid
+ FROM Exists_from
+ Inner Join Ingridient on rid=id,
+ Where id = ?
+ """, (name,))
+ rows = cur.fetchall()
+ return [dict(row) for row in rows]
+
diff --git a/project/backend/Recipe.py b/project/backend/Recipe.py
index 6578c7b..788664c 100644
--- a/project/backend/Recipe.py
+++ b/project/backend/Recipe.py
@@ -5,12 +5,13 @@
class Recipe:
def __init__(self, name: str, ingridients: list[Ingridient], description: str):
self.__name = name
- self.__Ingridienten = ingridients
+ self.__Ingridients = ingridients
self.__description = description
self.__original = ""
self.__duration = ""
- self.__rating = 0
+ self.__rating = 0.0
self.__countPersons = 1
+ self.__matching = 0
self.__database = Database()
def saveInDB(self) -> bool:
@@ -34,11 +35,17 @@ def getDescription(self) -> str:
def setDescription(self, description: str):
self.description = description
- def getRating(self) -> int:
- return self.rating
+ def getRating(self) -> float:
+ return self.__rating
- def setRating(self, rating: int):
- self.rating = rating
+ def setRating(self, rating: float):
+ self.__rating = rating
+
+ def getMatching(self) -> int:
+ return self.__matching
+
+ def incrementMatching(self):
+ self.__matching+=1
def getDuration(self) -> str:
return self.duration
@@ -46,9 +53,9 @@ def getDuration(self) -> str:
def setDuration(self, duration: str):
self.duration = duration
- def getIngridient(self) -> list[Ingridient]:
- return self.ingridients
+ def getIngridients(self) -> list[Ingridient]:
+ return self.__ingridients
def setIngridient(self, ingridients: list[Ingridient]):
- self.ingridients = ingridients
+ self.__Ingridients = ingridients
diff --git a/project/backend/RecipeSucuk.py b/project/backend/RecipeSucuk.py
new file mode 100644
index 0000000..a43033e
--- /dev/null
+++ b/project/backend/RecipeSucuk.py
@@ -0,0 +1,36 @@
+import Database
+from Ingridient import Ingridient
+from Recipe import Recipe
+from Database import getAllRecipes, getAllIngridientsForRecipe
+
+def findRecipes(ingriedents: list[Ingridient])-> list[Recipe]:
+ recipes = __initRecipes()
+
+ for ingridient in ingriedents:
+ recipes = __filterRecipes(recipes)
+
+ recipes.sort(key=lambda x: x.getRating(), reverse=True)
+ return recipes
+
+def __filterRecipes(recipes: list[Recipe], ingridient: Ingridient)-> list[Recipe]:
+ for recipe in recipes:
+ for recipeIngridient in recipe.getIngridients():
+ if recipeIngridient.getName() == ingridient.getName():
+ recipe.incrementMatching()
+ recipe.setRating(recipe.getMatching/ len(recipe.getIngridients))
+ return recipes
+
+def __initRecipes()-> list[Recipe]:
+ recipesRaw = getAllRecipes
+ recipes = []
+ for recipeRaw in recipesRaw:
+ recipe = Recipe(recipeRaw["name"], __formatIngridients(recipeRaw["id"]), recipeRaw["description"])
+ recipes.append(recipe)
+ return recipes
+
+def __formatIngridients(id: int)-> list[Ingridient]:
+ ingridientsRaw = getAllIngridientsForRecipe(id)
+ ingridients = []
+ for ingridientRaw in ingridientsRaw:
+ ingridients.append(Ingridient(ingridientRaw["name"],ingridientRaw["amount"]))
+ return ingridients
\ No newline at end of file
From ef434217862e589abe4f410977f1d5e66526d8be Mon Sep 17 00:00:00 2001
From: Samuel Goebel
Date: Mon, 4 May 2026 15:33:13 +0200
Subject: [PATCH 02/85] Added Filtering
---
project/backend/RecipeSucuk.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/project/backend/RecipeSucuk.py b/project/backend/RecipeSucuk.py
index a43033e..25d1e0c 100644
--- a/project/backend/RecipeSucuk.py
+++ b/project/backend/RecipeSucuk.py
@@ -10,6 +10,12 @@ def findRecipes(ingriedents: list[Ingridient])-> list[Recipe]:
recipes = __filterRecipes(recipes)
recipes.sort(key=lambda x: x.getRating(), reverse=True)
+
+ for recipe in recipes[::-1]:
+ if recipe.getMatching < 3:
+ recipes.remove(recipe)
+ else:
+ break
return recipes
def __filterRecipes(recipes: list[Recipe], ingridient: Ingridient)-> list[Recipe]:
From e701e833b318f40542b18c947822c353d65b88a9 Mon Sep 17 00:00:00 2001
From: nicla
Date: Mon, 4 May 2026 15:44:37 +0200
Subject: [PATCH 03/85] =?UTF-8?q?Tests=20f=C3=BCr=20Database,=20passwort?=
=?UTF-8?q?=20=C3=BCberpr=C3=BCfung=20und=20email=20=C3=BCber=C3=BCrufung?=
=?UTF-8?q?=20wurden=20geschrieben.=20Die=20dazugeh=C3=B6rigen=20Abh=C3=A4?=
=?UTF-8?q?ngigkeiten=20in=20requirements.txt=20angepasst.=20Sie=20werden?=
=?UTF-8?q?=20in=20test=5Fmain.py=20aufgerufen.=20Namens=20faktorisierung?=
=?UTF-8?q?=20ist=20schwer=20bei=20den=20Klassennamen=20da=20bei=20tests?=
=?UTF-8?q?=20der=20pycharm=20Debugger=20eher=20auf=20klassen=20mit=20test?=
=?UTF-8?q?=5F=20vorne=20guckt.=20Deswegen=20leider=20test=5F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
project/backend/Database.py | 7 ++-
project/backend/requirements.txt | 3 +-
project/backend/tests/__init__.py | 0
project/backend/tests/test_database.py | 72 +++++++++++++++++++++++++
project/backend/tests/test_email.py | 21 ++++++++
project/backend/tests/test_main.py | 25 +++++----
project/backend/tests/test_password.py | 41 ++++++++++++++
project/data/LazyCookDB.sqlite3 | Bin 65536 -> 65536 bytes
8 files changed, 157 insertions(+), 12 deletions(-)
create mode 100644 project/backend/tests/__init__.py
create mode 100644 project/backend/tests/test_database.py
create mode 100644 project/backend/tests/test_email.py
create mode 100644 project/backend/tests/test_password.py
diff --git a/project/backend/Database.py b/project/backend/Database.py
index 0d469cf..a2ab902 100644
--- a/project/backend/Database.py
+++ b/project/backend/Database.py
@@ -1,8 +1,13 @@
import sqlite3
from contextlib import contextmanager
from pathlib import Path
+import os
-DB_PATH = Path("/data/LazyCookDB.sqlite3")
+# Verwende die Datenbank aus dem data-Ordner
+DB_PATH = Path(__file__).parent.parent / "data" / "LazyCookDB.sqlite3"
+
+# Stelle sicher, dass das Verzeichnis existiert
+DB_PATH.parent.mkdir(parents=True, exist_ok=True)
def getConnection() -> sqlite3.Connection:
diff --git a/project/backend/requirements.txt b/project/backend/requirements.txt
index feac5cf..60137ce 100644
--- a/project/backend/requirements.txt
+++ b/project/backend/requirements.txt
@@ -4,4 +4,5 @@ uvicorn[standard]==0.38.0
gunicorn==23.0.0
bcrypt
python-jose[cryptography]
-python-multipart
\ No newline at end of file
+python-multipart
+pytest==9.0.3
\ No newline at end of file
diff --git a/project/backend/tests/__init__.py b/project/backend/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/project/backend/tests/test_database.py b/project/backend/tests/test_database.py
new file mode 100644
index 0000000..d6af6f1
--- /dev/null
+++ b/project/backend/tests/test_database.py
@@ -0,0 +1,72 @@
+import pytest
+import sys
+import os
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
+
+
+import Database
+from Auth import hashPassword, verifyPassword
+from Database import createAccount, getAccountByEmail, deleteAccount, updateAccount
+
+@pytest.fixture(autouse=True)
+def useTestDb():
+ Database.initDB()
+ yield
+ # Tabellen nach jedem Test leeren
+ con = Database.getConnection()
+ con.execute("DELETE FROM Account")
+ con.commit()
+ con.close()
+
+
+class TestCreateAccount:
+ def testAccountErstellen(self):
+ result = createAccount("test@example.com", "Test User", "hashedPW")
+ assert result is not None
+ assert result["email"] == "test@example.com"
+
+ def testDuplikatEmail(self):
+ createAccount("test@example.com", "Test User", "hashedPW")
+ result = createAccount("test@example.com", "Anderer User", "hashedPW")
+ assert result is None
+
+
+class TestGetAccountByEmail:
+ def testAccountGefunden(self):
+ createAccount("test@example.com", "Test User", "hashedPW")
+ account = getAccountByEmail("test@example.com")
+ assert account is not None
+ assert account["email"] == "test@example.com"
+
+ def testAccountNichtGefunden(self):
+ result = getAccountByEmail("nichtvorhanden@example.com")
+ assert result is None
+
+
+class TestDeleteAccount:
+ def testAccountLoeschen(self):
+ createAccount("test@example.com", "Test User", "hashedPW")
+ result = deleteAccount("test@example.com")
+ assert result is True
+ assert getAccountByEmail("test@example.com") is None
+
+ def testNichtVorhandenenAccountLoeschen(self):
+ result = deleteAccount("nichtvorhanden@example.com")
+ assert result is False
+
+
+class TestUpdateAccount:
+ def testEmailAendern(self):
+ account = createAccount("alt@example.com", "Test User", "hashedPW")
+ updateAccount(account["id"], email="neu@example.com")
+ assert getAccountByEmail("neu@example.com") is not None
+ assert getAccountByEmail("alt@example.com") is None
+
+ def testPasswortAendern(self):
+ account = createAccount("test@example.com", "Test User", hashPassword("Alt1!"))
+ neuerHash = hashPassword("Neu1!")
+ updateAccount(account["id"], password_hash=neuerHash)
+ aktuell = getAccountByEmail("test@example.com")
+ # Note: The Database has 'hashedPassword' column
+ assert verifyPassword("Neu1!", aktuell["hashedPassword"]) is True
\ No newline at end of file
diff --git a/project/backend/tests/test_email.py b/project/backend/tests/test_email.py
new file mode 100644
index 0000000..7225231
--- /dev/null
+++ b/project/backend/tests/test_email.py
@@ -0,0 +1,21 @@
+import pytest
+import sys
+import os
+
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
+
+from Auth import validateEmail
+
+class TestValidateEmail:
+ def testGueltigeEmail(self):
+ assert validateEmail("test@example.com") is None
+
+ def testFehlendesAt(self):
+ assert validateEmail("testexample.com") is not None
+
+ def testFehlendeDomain(self):
+ assert validateEmail("test@") is not None
+
+ def testLeereEmail(self):
+ assert validateEmail("") is not None
\ No newline at end of file
diff --git a/project/backend/tests/test_main.py b/project/backend/tests/test_main.py
index d5af60a..9370439 100644
--- a/project/backend/tests/test_main.py
+++ b/project/backend/tests/test_main.py
@@ -1,10 +1,15 @@
-import unittest
-
-
-class MyTestCase(unittest.TestCase):
- def test_something(self):
- self.assertEqual(True, True) # add assertion here
-
-
-if __name__ == '__main__':
- unittest.main()
+"""
+Test Runner - Führe alle Tests aus
+Nutzt pytest zum Sammeln und Ausführen aller Tests im tests-Ordner
+"""
+import pytest
+import sys
+import os
+
+if __name__ == "__main__":
+ # Suche alle Test-Dateien im aktuellen Verzeichnis und führe sie aus
+ pytest.main([
+ os.path.dirname(__file__),
+ "-v",
+ "--tb=short"
+ ])
diff --git a/project/backend/tests/test_password.py b/project/backend/tests/test_password.py
new file mode 100644
index 0000000..65ec47c
--- /dev/null
+++ b/project/backend/tests/test_password.py
@@ -0,0 +1,41 @@
+import pytest
+import sys
+import os
+
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
+
+from Auth import validatePassword, hashPassword, verifyPassword
+
+class TestValidatePassword:
+ def testGueltigesPasswort(self):
+ assert validatePassword("Sicher1!") is None
+
+ def testZuKurz(self):
+ assert validatePassword("Ab1!") is not None
+
+ def testKeinGrossbuchstabe(self):
+ assert validatePassword("sicher1!") is not None
+
+ def testKeinKleinbuchstabe(self):
+ assert validatePassword("SICHER1!") is not None
+
+ def testKeineZahl(self):
+ assert validatePassword("SicherAB!") is not None
+
+ def testKeinSonderzeichen(self):
+ assert validatePassword("Sicher123") is not None
+
+
+class TestPasswortHashing:
+ def testHashIstVerschieden(self):
+ hashed = hashPassword("Sicher1!")
+ assert hashed != "Sicher1!"
+
+ def testVerifyKorrektesPasswort(self):
+ hashed = hashPassword("Sicher1!")
+ assert verifyPassword("Sicher1!", hashed) is True
+
+ def testVerifyFalschesPasswort(self):
+ hashed = hashPassword("Sicher1!")
+ assert verifyPassword("Falsch1!", hashed) is False
\ No newline at end of file
diff --git a/project/data/LazyCookDB.sqlite3 b/project/data/LazyCookDB.sqlite3
index 8f8acc3ee212d9a9495fca4da0907069ff828483..aa7b38f1a5172558800258b27978e6ccc2143d12 100644
GIT binary patch
delta 486
zcmZo@U}k)5T;g-EVrFB|m6tUx
zNi8mMNUccBEyzjLOU};?0rC|>i&Kj-5{olZQv$+`42;Zl4NP?nOcV?atxSxq3@jNK
z7@ka4kk6}U$k%5Fn$pOM+ZYw2Bo#v=6_
zl8O@loJdPU&kWmY_#+>W}0jj7dE-x*BB_t#v0)7rAei;V-K7N_af(iosECo#L43ib)^Xdy2
z*%?eF8(EF>GLv%>i}i97ORBO{9m-Ra^ionoQj1Fz+*FK`R1A$&^o$Fg{0j@x-JSK^
z3SCRfJWP@eO8pC5vb@|~$_xTZGb)|(iyRI8jWQGc&2mGlyu-8HgN+P~%ybP*bd5|D
z3=OP|EUio|xEL51nE1~!@So*>!LV7#-~d016tgAc^65`CJoY)+)$>1SBBh%zZ
z_DVwR+}{}ZbNO!Zx$$n|mE-B+{~-kW@;
z{GPnEd~f-e0u4{+om?9m!xGKXXgT?AYy^a185h9{5@DRY*Eft6BK9#3D(M>^!4d_O
K+`KYAd;tI|b#)d1
From ef3fd46c1ce80eb915d45a07d9e931fb9a909b7b Mon Sep 17 00:00:00 2001
From: nicla
Date: Mon, 4 May 2026 15:50:11 +0200
Subject: [PATCH 04/85] =?UTF-8?q?ci=20pipeline=20abh=C3=A4gigkeit=20mit=20?=
=?UTF-8?q?rein=20gemacht?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/ci.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a682cc0..f2e2a00 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,7 +3,7 @@ on:
pull_request:
branches: [main, 'blatt*']
push:
- branches: [main, 'blatt*']
+ branches: [main, 'blatt*', 'feat/*']
permissions:
contents: read
@@ -50,7 +50,7 @@ jobs:
- name: Run backend tests
run: |
- docker-compose -f project/compose.yaml exec -T backend-dev sh -c "python -m unittest discover -s tests -v"
+ docker-compose -f project/compose.yaml exec -T backend-dev sh -c "python -m pytest tests/ -v"
# tests:
# runs-on: ubuntu-latest
From d2c72c060761ab910add1fd0de6e509b8f5f6077 Mon Sep 17 00:00:00 2001
From: nicla
Date: Tue, 5 May 2026 10:12:41 +0200
Subject: [PATCH 05/85] Feat bei Ci yml entfernt
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f2e2a00..20dbc68 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,7 +3,7 @@ on:
pull_request:
branches: [main, 'blatt*']
push:
- branches: [main, 'blatt*', 'feat/*']
+ branches: [main, 'blatt*']
permissions:
contents: read
From 70b7d97c85b21ffadd03b68f8d21fa8ecfa9364e Mon Sep 17 00:00:00 2001
From: F <150372421+GalacticCodeGambit@users.noreply.github.com>
Date: Tue, 5 May 2026 16:09:38 +0200
Subject: [PATCH 06/85] Delete project/data/LazyCookDB.sqlite3
---
project/data/LazyCookDB.sqlite3 | Bin 65536 -> 0 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 project/data/LazyCookDB.sqlite3
diff --git a/project/data/LazyCookDB.sqlite3 b/project/data/LazyCookDB.sqlite3
deleted file mode 100644
index 8f8acc3ee212d9a9495fca4da0907069ff828483..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 65536
zcmeI)&2Qr99S3m83rsRYNDdX6PSx>fW~8JUsBK~lxpWMe_uybm2=qXCG2n#vcaXMH
zI=kCy)mG}c=l%oz6DswvhgA>N9@=A1z4TU9J+x8{gap#i>^4MVcD_gigNMgIe}2E`
z!PpNIjf9kzOysMLa=JxWHQO4kR`Zr1G#bs0df!&>lbc>$SeraiAGK4<^A>kBmaNub
z{HMmK&1tqw#?KAE-~657=NsSK{DuBskN&OO)c;}i*Xw`O=2naInt1Iea=*_DIqf6vDi6+f>gajuD&;BaYH|Bx=84|)@}>4?)TnIup3-bJQ~5@;|zM=QNLaomfhPt;ih2gIh)L0c+ZFiXpP7U*}QezA{
zlmF$sQkyD~6gBl^>z#8`Z+iMv`)O)aZsU$I{`+{Q$R=H8Zn57zW=$2rv8d)uOpH1{
z7n1~kSl!qWAC~5K=)2pmq{}iP@uw1T*-;lU@O7r5UPOB{<)=12Q+`~#RjfAVl-^wV
zz5T~J(+{4`%l$ZWDQf)qA7y;J$?@NfnT$_@nY-P)Got>BDZU3uy1I$E
zdA~LCz5R{m~h++V$)A8H*oSZLZioy5D#>
zmip@F>Io;he4MyzC9enMTXQk2pgpTzXY#(7SH0JsDe8Lp(I?(Dz3KI9?PueoD^boj
z
ze-2dVmp{9%GX-DIEC1<|9S-5wWv)A3)tUD9wTaQdba`;ulUXa3o_R%Q;-1Ybp35YuA7z{}fB*y_009U<00Izz00cfjf!c~*asvU?YYG2}yBQDCByBU32WFZjDEjb#KD1IbtzN@CRkv7CvZZv>qF$39oXc;!
za>kOACArxmJeOOvSnE!qUMqNA7EhgTcYFs~vMtoyX92IfLmsz_eP^}7Q39=`1zR~b
z2*%I6(W@;U5EOYxI}Q)*n`&NHj9+MsUl{-T0j3zc3;_s000Izz00bZa0SG_<0uXrr
z1iroM(B6KoV#Tz|Y}|HI!C?Hm`u+b)jqyw4U*5kRM#K<+00bZa0SG_<0uX=z1Rwx`
z4_M%_cJ-Nd@?(wdmDShU>7R=Xf5-o1qBETTuYddxjj_D-=Pi%nw}$UN`AmI^8w4N#
z0SG_<0uX=z1R(H@5xCgV>owX7$B}iV8!>ZozCgLXX1-jgDm`yIZ*zuXmGgA;EMA}k
z_E;n1J&E?ig{~{ujVj_ny&ZEO54=b2UcOHzs>M#U+DW*XXgUz~QGy`L?ZSAbF{4gz
zmMH5X6{Fkdk3uG?pf1PrfOxQWZEyX
zwMeGn4<*{k)KRV%q`Qeu?$qq5c*Q);M~-TaQofuLj_a=Ed9sksHcGU!7w@?fiqHrv
z%|3S{Cid`vu`qUqu~WCkG+ckNvp$Kbl$WE?)AOP)Cb>=$R4c(ApM=ArP*3F?y}Z+U
zYz_n*tY^SbzCqD^%1Cap8jy>rPFkcZa;6$Q9SG5UxZ(^s2b`?Ly9eH;c*BWg4oTKx
zvyoQXaa&BI^%py9lb9H(+RBk$b2*(WWzRx2m(&kdyh1R+x0rO(9JC9CQcf&(Yra~C
z7Ui?EQXn5Z8gzngGn-PX1G?%DIGb)Rau&6@QxPs)76#@SC;E_4r>5I16lJGu%zwvp
zx&C5jbrMr|U}MQj)*P*!7NQ{~$(TE|Gn+i2!|rIEVFyy8WVZ>fu%ABb7fupgccMur
zLVSR6^~(oT-Q5WWA|9_Cjq{$a=iHvn4+f>ujMI3oJZ+&F)P4~Io
z$=i8*JRgx2?#yZPdHcQ?7hr8+-*rfoq)w){QbQ)vMzZ#sA&n;6kJevotMmIuA@%ct
zdOH~k_{~BgVzskfGCu
zfB*y_009U<00Izz00bZafdvcT`u~DK(HsOI009U<00Izz00bZa0SG`~2?hQG0wt;;
From 7b20faf1bf15bcddb1ab94fe18366bc4d963a05a Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 5 May 2026 16:25:22 +0200
Subject: [PATCH 07/85] Feature: Forgot Password link in signin modal. Popup
for Email Input. Konto deletion reactivation.
---
project/backend/Auth.py | 34 +++-
project/backend/Database.py | 65 +++++++-
project/backend/Models.py | 12 ++
project/backend/Routes.py | 51 +++++-
.../frontend/app/homepage/forgotPassword.tsx | 74 +++++++++
project/frontend/app/homepage/homepage.tsx | 9 +-
project/frontend/app/homepage/signin.tsx | 5 +-
.../frontend/app/profile/changeEmailPopup.tsx | 53 +++++++
.../app/profile/changePasswordPopup.tsx | 114 ++++++++++++++
project/frontend/app/profile/page.tsx | 148 ++----------------
project/frontend/app/profile/style.css | 10 ++
project/frontend/app/reset-password/page.tsx | 122 +++++++++++++++
12 files changed, 552 insertions(+), 145 deletions(-)
create mode 100644 project/frontend/app/homepage/forgotPassword.tsx
create mode 100644 project/frontend/app/profile/changeEmailPopup.tsx
create mode 100644 project/frontend/app/profile/changePasswordPopup.tsx
create mode 100644 project/frontend/app/profile/style.css
create mode 100644 project/frontend/app/reset-password/page.tsx
diff --git a/project/backend/Auth.py b/project/backend/Auth.py
index 41283b0..1ffde5d 100644
--- a/project/backend/Auth.py
+++ b/project/backend/Auth.py
@@ -13,7 +13,11 @@
import bcrypt
from pydantic import BaseModel
-from Models import Token, User
+import hashlib
+
+PASSWORD_RESET_EXPIRE_MINUTES = 30
+
+from Models import Token, User, ForgotPasswordRequest, ResetPasswordRequest
from Database import getAccountByEmail, saveRefreshToken, getRefreshToken
@@ -116,3 +120,31 @@ async def getCurrentUser(token: Annotated[str, Depends(oauth2_scheme)]) -> User:
raise credentials_exception
return User(email=konto["email"], name=konto["name"])
+
+# --- Reset Password --- #
+
+def hashResetToken(token: str) -> str:
+ """SHA-256 Hash – für Reset-Tokens reicht das, sie sind ohnehin hochentropisch."""
+ return hashlib.sha256(token.encode()).hexdigest()
+
+
+def createPasswordResetToken(kontoId: int) -> str:
+ """Generiert Klartext-Token (geht per Mail) und speichert nur den Hash in der DB."""
+ from Database import savePasswordResetToken
+ token = secrets.token_urlsafe(48)
+ expiresAt = datetime.now(timezone.utc) + timedelta(minutes=PASSWORD_RESET_EXPIRE_MINUTES)
+ savePasswordResetToken(kontoId, hashResetToken(token), expiresAt.isoformat())
+ return token
+
+
+def validatePasswordResetToken(token: str) -> dict | None:
+ from Database import getPasswordResetToken
+ entry = getPasswordResetToken(hashResetToken(token))
+ if entry is None or entry["used_at"] is not None:
+ return None
+ expiresAt = datetime.fromisoformat(entry["expires_at"])
+ if expiresAt.tzinfo is None:
+ expiresAt = expiresAt.replace(tzinfo=timezone.utc)
+ if expiresAt < datetime.now(timezone.utc):
+ return None
+ return entry
\ No newline at end of file
diff --git a/project/backend/Database.py b/project/backend/Database.py
index 0d469cf..4601e4c 100644
--- a/project/backend/Database.py
+++ b/project/backend/Database.py
@@ -99,6 +99,17 @@ def initDB():
UNIQUE (AccountID, rid)
)
""")
+ cur.execute("""
+ CREATE TABLE IF NOT EXISTS PasswordResetToken (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ kontoID INTEGER NOT NULL,
+ tokenHash TEXT NOT NULL UNIQUE,
+ expiresAt TIMESTAMP NOT NULL,
+ usedAt TIMESTAMP,
+ createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (kontoID) REFERENCES Account (id) ON DELETE CASCADE
+ )
+ """)
print("✅ Datenbank-Tabellen erfolgreich initialisiert")
@@ -203,4 +214,56 @@ def cleanupExpiredTokens() -> None:
"""Löscht alle abgelaufenen Refresh Tokens."""
with getDB() as con:
cur = con.cursor()
- cur.execute("DELETE FROM RefreshToken WHERE expiresAt < datetime('now')")
\ No newline at end of file
+ cur.execute("DELETE FROM RefreshToken WHERE expiresAt < datetime('now')")
+
+
+
+# ── Password-Reset-Token-Operationen ───────────────────────────
+
+def savePasswordResetToken(kontoID: int, tokenHash: str, expiresAt: str) -> None:
+ """Invalidiert alte Tokens des Kontos und speichert einen neuen."""
+ with getDB() as con:
+ cur = con.cursor()
+ # Alte, ungenutzte Tokens für dieses Konto invalidieren
+ cur.execute(
+ "UPDATE PasswordResetToken SET used_at = CURRENT_TIMESTAMP "
+ "WHERE kontoID = ? AND usedAt IS NULL",
+ (kontoID,),
+ )
+ cur.execute(
+ "INSERT INTO PasswordResetToken (kontoID, tokenHash, expiresAt) VALUES (?, ?, ?)",
+ (kontoID, tokenHash, expiresAt),
+ )
+
+
+def getPasswordResetToken(tokenHash: str) -> dict | None:
+ con = getConnection()
+ try:
+ cur = con.cursor()
+ cur.execute(
+ "SELECT id, kontoID, tokenHash, expiresAt, usedAt "
+ "FROM PasswordResetToken WHERE tokenHash = ?",
+ (tokenHash,),
+ )
+ row = cur.fetchone()
+ return dict(row) if row else None
+ finally:
+ con.close()
+
+
+def markResetTokenUsed(tokenID: int) -> None:
+ with getDB() as con:
+ cur = con.cursor()
+ cur.execute(
+ "UPDATE PasswordResetToken SET usedAt = CURRENT_TIMESTAMP WHERE id = ?",
+ (token_id,),
+ )
+
+
+def updateKontoPassword(konto_id: int, hashed_password: str) -> None:
+ with getDB() as con:
+ cur = con.cursor()
+ cur.execute(
+ "UPDATE Account SET hashed_password = ? WHERE id = ?",
+ (hashed_password, konto_id),
+ )
\ No newline at end of file
diff --git a/project/backend/Models.py b/project/backend/Models.py
index 43c573e..86638ac 100644
--- a/project/backend/Models.py
+++ b/project/backend/Models.py
@@ -19,3 +19,15 @@ class RefreshRequest(BaseModel):
class LogoutRequest(BaseModel):
refresh_token: str
+
+class ForgotPasswordRequest(BaseModel):
+ email: str
+
+class ResetPasswordRequest(BaseModel):
+ token: str
+ new_password: str
+
+class UpdateUser(BaseModel):
+ email: str | None = None
+ currentPassword: str | None = None
+ newPassword: str | None = None
\ No newline at end of file
diff --git a/project/backend/Routes.py b/project/backend/Routes.py
index 0289641..1c9a077 100644
--- a/project/backend/Routes.py
+++ b/project/backend/Routes.py
@@ -14,6 +14,8 @@
validateRefreshToken,
verifyPassword,
createAccessToken,
+ createPasswordResetToken,
+ validatePasswordResetToken,
)
from Database import (
createAccount,
@@ -22,9 +24,13 @@
deleteAllRefreshTokens,
deleteAccount,
updateAccount,
+ markResetTokenUsed,
+ updateKontoPassword,
+ deleteAllRefreshTokens
+
)
-from Models import User, Token, UserCreate, RefreshRequest, LogoutRequest
+from Models import User, Token, UserCreate, RefreshRequest, LogoutRequest, ForgotPasswordRequest, ResetPasswordRequest, UpdateUser
router = APIRouter()
@@ -104,10 +110,7 @@ async def deleteCurrentUser(currentUser: Annotated[User, Depends(getCurrentUser)
# ── Account aktualisieren ────────────────────────────────────────
-class UpdateUser(_BaseModel):
- email: str | None = None
- currentPassword: str | None = None
- newPassword: str | None = None
+
@router.patch("/users/me")
async def updateCurrentUser(
@@ -143,3 +146,41 @@ async def updateCurrentUser(
print(f"E-Mail konnte nicht gesendet werden: {e}")
return {"success": True}
+
+# ------------ Passwort vergessen ----------------- #
+
+@router.post("/auth/forgot-password")
+async def forgotPassword(body: ForgotPasswordRequest):
+ """Schritt 1: User gibt E-Mail ein, bekommt Reset-Link per Mail."""
+ konto = getAccountByEmail(body.email)
+
+ # Token nur erzeugen wenn Konto existiert – aber IMMER gleiche Antwort senden!
+ if konto is not None:
+ token = createPasswordResetToken(konto["id"])
+ resetLink = f"http://localhost:8000/reset-password?token={token}"
+ # TODO: Mail versenden – siehe Hinweis unten
+ print(f"[DEV] Reset-Link für {body.email}: {resetLink}")
+
+ return {"detail": "Falls die E-Mail existiert, wurde ein Link versendet."}
+
+
+@router.post("/auth/reset-password")
+async def resetPassword(body: ResetPasswordRequest):
+ """Schritt 2: User setzt mit Token aus Mail das neue Passwort."""
+ pwError = validatePassword(body.new_password)
+ if pwError:
+ raise HTTPException(status_code=400, detail=pwError)
+
+ entry = validatePasswordResetToken(body.token)
+ if entry is None:
+ raise HTTPException(
+ status_code=400,
+ detail="Link ungültig oder abgelaufen. Bitte neuen anfordern.",
+ )
+
+ updateKontoPassword(entry["konto_id"], hashPassword(body.new_password))
+ markResetTokenUsed(entry["id"])
+ # Sicherheitsmaßnahme: Alle aktiven Sessions ungültig machen
+ deleteAllRefreshTokens(entry["konto_id"])
+
+ return {"detail": "Passwort erfolgreich zurückgesetzt."}
diff --git a/project/frontend/app/homepage/forgotPassword.tsx b/project/frontend/app/homepage/forgotPassword.tsx
new file mode 100644
index 0000000..cd778d1
--- /dev/null
+++ b/project/frontend/app/homepage/forgotPassword.tsx
@@ -0,0 +1,74 @@
+import React, { useState } from "react";
+import Field from "@/app/components/fields";
+import styles from "./page.module.css";
+
+const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";
+
+export default function ForgotPasswordForm({ onClose, onBack,}: { onClose: () => void; onBack: () => void; }) {
+ const [email, setEmail] = useState("");
+ const [busy, setBusy] = useState(false);
+ const [submitted, setSubmitted] = useState(false);
+ const [emailBlurred, setEmailBlurred] = useState(false);
+
+ const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
+
+ const handleSubmit = async () => {
+ setEmailBlurred(true);
+ if (!emailValid) return;
+ setBusy(true);
+ try {
+ await fetch(`${API_URL}/auth/forgot-password`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email }),
+ });
+ setSubmitted(true);
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ if (submitted) {
+ return (
+
+
E-Mail versendet
+
+ Falls ein Konto mit dieser E-Mail existiert, haben wir dir einen
+ Link zum Zurücksetzen geschickt. Prüfe auch deinen Spam-Ordner.
+
+
+
+ );
+ }
+
+ return (
+
+
Passwort vergessen
+
+ Gib deine E-Mail-Adresse ein. Wir schicken dir einen Link zum
+ Zurücksetzen.
+
+
setEmail(v)}
+ placeholder="Email"
+ onBlur={() => setEmailBlurred(true)}
+ state={
+ emailBlurred && !emailValid ? "error" : emailValid ? "success" : "default"
+ }
+ />
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/project/frontend/app/homepage/homepage.tsx b/project/frontend/app/homepage/homepage.tsx
index 5c04f67..6ad3795 100644
--- a/project/frontend/app/homepage/homepage.tsx
+++ b/project/frontend/app/homepage/homepage.tsx
@@ -7,11 +7,12 @@ import {useCallback, useState} from "react";
import Modal from "@/app/components/modal";
import LoginForm from "@/app/homepage/signin";
import RegisterForm from "@/app/homepage/signup";
+import ForgotPasswordForm from "@/app/homepage/forgotPassword";
export default function Homepage() {
- const [modal, setModal] = useState<"login" | "register" | null>(null);
+ const [modal, setModal] = useState<"login" | "register" | "forgot"| null>(null);
const close = useCallback(() => setModal(null), []);
@@ -191,12 +192,16 @@ export default function Homepage() {
{/* ── Modals ──────────────────────────────────────────── */}
- setModal("register")} />
+ setModal("register")} onForgot={() => setModal("forgot")} />
setModal("login")} />
+
+
+ setModal("login")} />
+
);
}
\ No newline at end of file
diff --git a/project/frontend/app/homepage/signin.tsx b/project/frontend/app/homepage/signin.tsx
index 1e40ca9..04df94a 100644
--- a/project/frontend/app/homepage/signin.tsx
+++ b/project/frontend/app/homepage/signin.tsx
@@ -5,7 +5,7 @@ import Field from "@/app/components/fields"
import styles from "./page.module.css";
import {useRouter} from "next/navigation";
-export default function LoginForm({ onClose, onSwitch }: { onClose: () => void; onSwitch: () => void }) {
+export default function LoginForm({ onClose, onSwitch, onForgot}: { onClose: () => void; onSwitch: () => void; onForgot: () => void }) {
const { login } = useAuth();
const router = useRouter();
const [email, setEmail] = useState("");
@@ -47,6 +47,9 @@ export default function LoginForm({ onClose, onSwitch }: { onClose: () => void;
{error && {error}
}
{ setEmail(v); if (emailBlurred) setEmailBlurred(false); }} placeholder="Email" onBlur={() => setEmailBlurred(true)} state={emailState()} />
e.key === "Enter" && handleSubmit()} onBlur={() => setPwBlurred(true)} state={pwBlurred && !password ? "error" : "default"} />
+
+
+
diff --git a/project/frontend/app/profile/changeEmailPopup.tsx b/project/frontend/app/profile/changeEmailPopup.tsx
new file mode 100644
index 0000000..9b971ea
--- /dev/null
+++ b/project/frontend/app/profile/changeEmailPopup.tsx
@@ -0,0 +1,53 @@
+import {Button} from "@/app/components/ui/button";
+import {fetchWithAuth} from "@/lib/auth";
+import {useState} from "react";
+
+const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";
+export default function ChangeEmail (){
+
+
+ const [newEmail, setNewEmail] = useState("");
+ const [emailMsg, setEmailMsg] = useState("");
+
+ async function handleEmailChange() {
+ setEmailMsg("");
+ try {
+ const res = await fetchWithAuth(`${API_URL}/users/me`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email: newEmail }),
+ });
+ if (!res.ok) {
+ const err = await res.json();
+ setEmailMsg(`${err.detail}`);
+ return;
+ }
+ setEmailMsg("E-Mail erfolgreich geändert.");
+ setNewEmail("");
+ } catch {
+ setEmailMsg("Unbekannter Fehler.");
+ }
+ }
+
+ return (
+
+
E-Mail ändern
+
setNewEmail(e.target.value)}
+ className="border rounded-lg px-3 py-2 text-sm w-full"
+ />
+ {emailMsg &&
{emailMsg}
}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/project/frontend/app/profile/changePasswordPopup.tsx b/project/frontend/app/profile/changePasswordPopup.tsx
new file mode 100644
index 0000000..3c6bce4
--- /dev/null
+++ b/project/frontend/app/profile/changePasswordPopup.tsx
@@ -0,0 +1,114 @@
+import {fetchWithAuth} from "@/lib/auth";
+import {useState} from "react";
+import {Button} from "@/app/components/ui/button";
+
+const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";
+
+type Modus = "change" | "forgot";
+
+interface ChangePasswordProps {
+ modus: Modus;
+ onSuccess?: () => void;
+}
+
+export default function ChangePassword({ modus, onSuccess }: ChangePasswordProps) {
+
+ const [currentPassword, setCurrentPassword] = useState("");
+ const [newPassword, setNewPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [passwordMsg, setPasswordMsg] = useState("");
+
+ const isForgot = modus === "forgot";
+
+ async function handlePasswordChange() {
+ setPasswordMsg("");
+
+ // Bestätigung prüfen (nur im forgot-Modus relevant, aber sinnvoll auch beim Ändern)
+ if (newPassword !== confirmPassword) {
+ setPasswordMsg("Die Passwörter stimmen nicht überein.");
+ return;
+ }
+
+ try {
+ const endpoint = isForgot
+ ? `${API_URL}/users/forgot-password`
+ : `${API_URL}/users/me`;
+
+ const body = isForgot
+ ? { newPassword }
+ : { currentPassword, newPassword };
+
+ const fetcher = isForgot ? fetch : fetchWithAuth;
+
+ const res = await fetcher(endpoint, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ });
+
+ if (!res.ok) {
+ const err = await res.json();
+ setPasswordMsg(`❌ ${err.detail}`);
+ return;
+ }
+
+ setPasswordMsg(
+ isForgot
+ ? "Passwort erfolgreich zurückgesetzt."
+ : "Passwort erfolgreich geändert."
+ );
+ setCurrentPassword("");
+ setNewPassword("");
+ setConfirmPassword("");
+ onSuccess?.();
+ } catch {
+ setPasswordMsg("Unbekannter Fehler.");
+ }
+ }
+
+ return (
+
+
+ {isForgot ? "Passwort zurücksetzen" : "Passwort ändern"}
+
+
+ {!isForgot && (
+
setCurrentPassword(e.target.value)}
+ className="border rounded-lg px-3 py-2 text-sm w-full"
+ />
+ )}
+
+
setNewPassword(e.target.value)}
+ className="border rounded-lg px-3 py-2 text-sm w-full"
+ />
+
+
setConfirmPassword(e.target.value)}
+ className="border rounded-lg px-3 py-2 text-sm w-full"
+ />
+
+ {passwordMsg &&
{passwordMsg}
}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/project/frontend/app/profile/page.tsx b/project/frontend/app/profile/page.tsx
index 1f5a269..2147626 100644
--- a/project/frontend/app/profile/page.tsx
+++ b/project/frontend/app/profile/page.tsx
@@ -1,17 +1,18 @@
"use client";
-import { useRef, useState} from "react";
+import {useState} from "react";
import { useAuth, fetchWithAuth } from "@/lib/auth";
import { useRouter } from "next/navigation";
import {ChefHat} from "lucide-react";
import { Button } from '../components/ui/button';
import ProfileDropdown from "@/app/components/profile_dropdown";
+import Modal from "@/app/components/modal";
+import ChangeEmail from "@/app/profile/changeEmailPopup";
+import ChangePassword from "@/app/profile/changePasswordPopup";
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";
export default function Profile() {
-
- const menuRef = useRef(null);
const { user, logout } = useAuth();
const router = useRouter();
const [showConfirm, setShowConfirm] = useState(false);
@@ -19,58 +20,8 @@ export default function Profile() {
const [showEmailModal, setShowEmailModal] = useState(false);
const [showPasswordModal, setShowPasswordModal] = useState(false);
- // E-Mail ändern
- const [newEmail, setNewEmail] = useState("");
- const [emailMsg, setEmailMsg] = useState("");
-
- // Passwort ändern
- const [currentPassword, setCurrentPassword] = useState("");
- const [newPassword, setNewPassword] = useState("");
- const [passwordMsg, setPasswordMsg] = useState("");
-
if (!user) return null;
- async function handleEmailChange() {
- setEmailMsg("");
- try {
- const res = await fetchWithAuth(`${API_URL}/users/me`, {
- method: "PATCH",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ email: newEmail }),
- });
- if (!res.ok) {
- const err = await res.json();
- setEmailMsg(`${err.detail}`);
- return;
- }
- setEmailMsg("E-Mail erfolgreich geändert.");
- setNewEmail("");
- } catch {
- setEmailMsg("Unbekannter Fehler.");
- }
- }
-
- async function handlePasswordChange() {
- setPasswordMsg("");
- try {
- const res = await fetchWithAuth(`${API_URL}/users/me`, {
- method: "PATCH",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ currentPassword, newPassword }),
- });
- if (!res.ok) {
- const err = await res.json();
- setPasswordMsg(`❌ ${err.detail}`);
- return;
- }
- setPasswordMsg("Passwort erfolgreich geändert.");
- setCurrentPassword("");
- setNewPassword("");
- } catch {
- setPasswordMsg("Unbekannter Fehler.");
- }
- }
-
async function handleAccountDeletion() {
const res = await fetchWithAuth(`${API_URL}/users/me`, { method: "DELETE" });
if (!res.ok) throw new Error("Konto löschen fehlgeschlagen");
@@ -111,13 +62,13 @@ export default function Profile() {
Einstellungen
- {/* E-Mail Modal */}
- {showEmailModal && (
- setShowEmailModal(false)}
- >
-
e.stopPropagation()}
- >
-
E-Mail ändern
-
setNewEmail(e.target.value)}
- className="border rounded-lg px-3 py-2 text-sm w-full"
- />
- {emailMsg &&
{emailMsg}
}
-
-
-
-
-
-
- )}
+ setShowEmailModal(false)}>
+
+
- {/* Passwort Modal */}
- {showPasswordModal && (
- setShowPasswordModal(false)}
- >
-
e.stopPropagation()}
- >
-
Passwort ändern
-
setCurrentPassword(e.target.value)}
- className="border rounded-lg px-3 py-2 text-sm w-full"
- />
-
setNewPassword(e.target.value)}
- className="border rounded-lg px-3 py-2 text-sm w-full"
- />
- {passwordMsg &&
{passwordMsg}
}
-
-
-
-
-
-
- )}
+ setShowPasswordModal(false)}>
+
+
);
}
diff --git a/project/frontend/app/profile/style.css b/project/frontend/app/profile/style.css
new file mode 100644
index 0000000..ef2f8da
--- /dev/null
+++ b/project/frontend/app/profile/style.css
@@ -0,0 +1,10 @@
+.popup {
+ background: #fff;
+ border-radius: 12px;
+ padding: 2rem 2rem 1.75rem;
+ width: auto;
+ max-width: 520px;
+ position: relative;
+ box-shadow: 0 16px 40px rgba(0, 0, 0, 0.12);
+ animation: slideUp 0.2s ease-out;
+}
\ No newline at end of file
diff --git a/project/frontend/app/reset-password/page.tsx b/project/frontend/app/reset-password/page.tsx
new file mode 100644
index 0000000..a56795c
--- /dev/null
+++ b/project/frontend/app/reset-password/page.tsx
@@ -0,0 +1,122 @@
+"use client";
+
+import React, { Suspense, useState } from "react";
+import { useSearchParams, useRouter } from "next/navigation";
+import Field from "@/app/components/fields";
+import styles from "@/app/homepage/page.module.css";
+
+const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";
+
+function ResetPasswordContent() {
+ const params = useSearchParams();
+ const router = useRouter();
+ const token = params.get("token") ?? "";
+
+ const [password, setPassword] = useState("");
+ const [confirm, setConfirm] = useState("");
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState("");
+ const [done, setDone] = useState(false);
+
+ const pwValid =
+ password.length >= 8 &&
+ /[A-Z]/.test(password) &&
+ /[a-z]/.test(password) &&
+ /[0-9]/.test(password) &&
+ /[^A-Za-z0-9]/.test(password);
+ const pwMatch = password === confirm && confirm.length > 0;
+
+ const handleSubmit = async () => {
+ setError("");
+ if (!pwValid) {
+ setError("Passwort erfüllt nicht alle Anforderungen.");
+ return;
+ }
+ if (!pwMatch) {
+ setError("Passwörter stimmen nicht überein.");
+ return;
+ }
+ setBusy(true);
+ try {
+ const res = await fetch(`${API_URL}/auth/reset-password`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ token, new_password: password }),
+ });
+ if (!res.ok) {
+ const data = await res.json();
+ setError(data.detail ?? "Fehler beim Zurücksetzen.");
+ return;
+ }
+ setDone(true);
+ setTimeout(() => router.push("/"), 2500);
+ } catch {
+ setError("Verbindungsfehler.");
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ if (!token) {
+ return (
+
+
Ungültiger Link
+
+ Der Link ist unvollständig. Bitte fordere einen neuen an.
+
+
+ );
+ }
+
+ if (done) {
+ return (
+
+
Erledigt!
+
+ Dein Passwort wurde zurückgesetzt. Du wirst gleich weitergeleitet…
+
+
+ );
+ }
+
+ return (
+
+
Neues Passwort festlegen
+
Wähle ein sicheres neues Passwort.
+ {error &&
{error}
}
+
+
+
+
+ );
+}
+
+export default function ResetPasswordPage() {
+ return (
+
+ Wird geladen…
+
+ }
+ >
+
+
+ );
+}
\ No newline at end of file
From 08852138d9175e22a6461f621f34f3f526009e42 Mon Sep 17 00:00:00 2001
From: F <150372421+GalacticCodeGambit@users.noreply.github.com>
Date: Tue, 5 May 2026 16:56:43 +0200
Subject: [PATCH 08/85] Feat/ci cd add lint (#142)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Rename models.py to Models.py
* Rename auth.py to Auth.py
* Rename Routes.py to routes.py
* Added Refactoring Summary
* Refactor documentation for code naming conventions
* Fix spelling errors in refactoring documentation
Corrected spelling of 'Refaktorisierung' and 'Frontend'.
* Delete project/data/LazyCookDB.sqlite3
* AI Agent md's changed
* Passwort und Email Änderung zu Popups geändert. und neugeordnet untereinander
* Update README.md to mark tasks as completed for account management features
* Add CI/CD pipeline and Super Linter configuration
* Test für lint
* Test ci/cd lint.yml
* Test ci/cd lint.yml
* Test ci/cd lint.yml
* Test ci/cd lint.yml regex Filter
* Test ci/cd lint.yml
* Test ci/cd lint.yml
* Test ci/cd lint.yml regex Filter angepasst
* Tests für Database, passwort überprüfung und email überürufung wurden geschrieben. Die dazugehörigen Abhängigkeiten in requirements.txt angepasst. Sie werden in test_main.py aufgerufen.
Namens faktorisierung ist schwer bei den Klassennamen da bei tests der pycharm Debugger eher auf klassen mit test_ vorne guckt. Deswegen leider test_
* ci pipeline abhägigkeit mit rein gemacht
* Feat bei Ci yml entfernt
* Update FILTER_REGEX_INCLUDE pattern in lint.yml
* Update lint.yml
* Enable validation for the entire codebase
* Update lint.yml
* Update lint.yml
* Enable validation for all codebase in lint workflow
* Remove Python Black and Markdown validation
* Update lint workflow to include additional validations
* Update FILTER_REGEX_INCLUDE to support YAML files
* Update lint.yml
* Update lint.yml
* Refactor CI configuration to use Docker Compose v2 syntax and add GitHub Super Linter step
* Test ci.yml
* Update ci.yml
* Enable Flake8 validation for Python files
* Test ci.yml
* Remove 'feat/*' branch from push trigger in CI configuration
* Python black gelöst
---------
Co-authored-by: Samuel Goebel
Co-authored-by: Eden Tabea Bernhard <105359952+EdenBernhard@users.noreply.github.com>
Co-authored-by: Nicoolaus <162422307+Nicoolaus@users.noreply.github.com>
Co-authored-by: Hellocrafting
Co-authored-by: nicla
Co-authored-by: Hellocrafting <75727565+Hellocrafting@users.noreply.github.com>
---
.github/workflows/ci.yml | 105 ++++++++--------------
README.md | 4 +
project/backend/Admin.py | 1 -
project/backend/Auth.py | 28 ++++--
project/backend/Database.py | 22 +++--
project/backend/EmailService.py | 3 +-
project/backend/Ingridient.py | 2 +-
project/backend/LazyCookAdministration.py | 1 -
project/backend/Models.py | 5 ++
project/backend/Person.py | 2 +-
project/backend/Recipe.py | 1 -
project/backend/Routes.py | 7 +-
project/backend/User.py | 1 -
project/backend/requirements.txt | 3 +-
project/backend/tests/__init__.py | 0
project/backend/tests/test_database.py | 73 +++++++++++++++
project/backend/tests/test_email.py | 21 +++++
project/backend/tests/test_main.py | 22 ++---
project/backend/tests/test_password.py | 41 +++++++++
19 files changed, 241 insertions(+), 101 deletions(-)
create mode 100644 project/backend/tests/__init__.py
create mode 100644 project/backend/tests/test_database.py
create mode 100644 project/backend/tests/test_email.py
create mode 100644 project/backend/tests/test_password.py
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a682cc0..d4c93fd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,31 +1,29 @@
name: "CI/CD Pipeline"
+
on:
pull_request:
branches: [main, 'blatt*']
push:
branches: [main, 'blatt*']
+
permissions:
contents: read
jobs:
- build:
+ build-tests:
+ name: Build stack and run tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - name: Install Docker Compose (build job)
- run: |
- sudo apt-get update
- sudo apt-get install -y docker-compose
-
- name: Build the stack
- run: docker-compose -f project/compose.yaml build
+ run: docker compose -f project/compose.yaml build
- name: Start services
run: |
# remove stale containers/volumes from previous runs that can cause compose to read invalid metadata
- docker-compose -f project/compose.yaml down --remove-orphans --volumes || true
- docker-compose -f project/compose.yaml up -d
+ docker compose -f project/compose.yaml down --remove-orphans --volumes || true
+ docker compose -f project/compose.yaml up -d
- name: Wait for frontend on :8000
run: |
@@ -42,69 +40,42 @@ jobs:
sleep $wait_seconds
done
echo "service not ready" >&2
- docker-compose -f project/compose.yaml ps || true
- docker-compose -f project/compose.yaml logs || true
+ docker compose -f project/compose.yaml ps || true
+ docker compose -f project/compose.yaml logs || true
exit 1
+
- name: Run smoke test frontend
run: curl -fsSL -o /dev/null http://localhost:8000/
- name: Run backend tests
run: |
- docker-compose -f project/compose.yaml exec -T backend-dev sh -c "python -m unittest discover -s tests -v"
+ docker compose -f project/compose.yaml exec -T backend-dev sh -c "python -m pytest tests/ -v"
-# tests:
-# runs-on: ubuntu-latest
-# steps:
-# - uses: actions/checkout@v4
-#
-# - name: Install Docker Compose (tests job)
-# run: |
-# sudo apt-get update
-# sudo apt-get install -y docker-compose
-#
-# - name: Pull images (if available) or build
-# run: |
-# # Versuche remote images zu ziehen; falls nicht vorhanden, baue lokal neu.
-# docker-compose -f project/compose.yaml pull || docker-compose -f project/compose.yaml build
-#
-# - name: Start services
-# run: |
-# docker-compose -f project/compose.yaml down --remove-orphans --volumes || true
-# docker-compose -f project/compose.yaml up -d backend-dev
-#
-# - name: Wait for backend to be ready
-# run: |
-# for i in $(seq 1 20); do
-# docker-compose -f project/compose.yaml exec -T backend-dev sh -c "python -c 'print(\"ok\")'" >/dev/null 2>&1 && break
-# sleep 1
-# done
-#
-# - name: Run backend tests
-# run: |
-# docker-compose -f project/compose.yaml exec -T backend-dev sh -c "python -m unittest discover -s tests -v"
+ linter:
+ name: Lint code base
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
-# lint:
-# runs-on: ubuntu-latest
-# needs: build
-# steps:
-# - name: Wait for frontend on :8000
-# run: |
-# PORT=8000
-# attempts=30
-# wait_seconds=3
-# for i in $(seq 1 $attempts); do
-# if curl -fsSL "http://localhost:${PORT}/" >/dev/null 2>&1; then
-# echo "ready"
-# exit 0
-# fi
-# echo "waiting... ($i)"
-# sleep $wait_seconds
-# done
-# echo "service not ready" >&2
-# docker-compose -f project/compose.yaml ps || true
-# docker-compose -f project/compose.yaml logs || true
-# exit 1
-# - name: Run linting tests
-# run: |
-# # startet einen einmaligen Container für den Service 'backend' und führt Linting-Tests aus (z.B. mit flake8)
-# docker-compose -f project/compose.yaml run --rm backend sh -c "flake8 ."
\ No newline at end of file
+ - name: Run GitHub Super Linter
+ uses: super-linter/super-linter/slim@v7
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ DEFAULT_BRANCH: main
+ VALIDATE_ALL_CODEBASE: false
+ # Root ignore file
+ # GitHub workflows
+ # Project backend/frontend + Dockerfiles + compose files
+ FILTER_REGEX_INCLUDE: '^/github/workspace/(\.gitignore|\.github/workflows/.*\.ya?ml|project/(backend/.*\.py|frontend/.*\.(ts|tsx|js|jsx|css|mjs|cjs)|Dockerfile[^/]*|(compose|docker-compose)\.ya?ml))$'
+ VALIDATE_PYTHON: true
+ VALIDATE_PYTHON_BLACK: true
+ VALIDATE_PYTHON_FLAKE8: true
+ VALIDATE_TYPESCRIPT_ES: true
+ VALIDATE_JAVASCRIPT_ES: true
+ VALIDATE_CSS: true
+ VALIDATE_DOCKERFILE: true
+ VALIDATE_YAML: true
+ VALIDATE_GITHUB_ACTIONS: true
diff --git a/README.md b/README.md
index 65a05fa..5beb5c9 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,10 @@ LazyCook soll eine Webanwendung sein. Die es ermöglichen seine vorhanden Zutate
- Containerization: Docker
- Projekt Management: GitHub Projects
+## Code-Quality / Linting
+- Der Workflow `.github/workflows/ci.yml` prüft mit dem Job `linter` das Projekt mit **GitHub Super-Linter**.
+- Dabei werden u. a. Python-, TypeScript-, JavaScript-, YAML- und Dockerfile-Dateien validiert.
+
+Die Anwendung sollte jetzt unter `http://localhost:8000` erreichbar sein.
+
+Für die Funktion [#115](https://github.com/GalacticCodeGambit/LazyCook/issues/115) von Email Versenden/Empfangen muss im Ordner `project/` eine `.env` Datei mit den folgenden Variablen angelegt werden:
+```
+EMAIL_HOST=
+GMAIL_PASSWORD=
+```
+
## Verwendete Technologien
- Frontend: HTML, CSS, TypeScript/React
- Backend: Python
@@ -30,26 +47,29 @@ LazyCook soll eine Webanwendung sein. Die es ermöglichen seine vorhanden Zutate
- Containerization: Docker
- Projekt Management: GitHub Projects
-## Code-Quality / Linting
-- Der Workflow `.github/workflows/ci.yml` prüft mit dem Job `linter` das Projekt mit **GitHub Super-Linter**.
-- Dabei werden u. a. Python-, TypeScript-, JavaScript-, YAML- und Dockerfile-Dateien validiert.
+### Code-Quality / Linting
+- Der Workflow `.github/workflows/lint.yml` prüft das Projekt mit **GitHub Super-Linter**.
+- Dabei werden u.a. Python-, TypeScript-, JavaScript-, YAML- und Dockerfile-Dateien validiert.
- ## Installation and Setup
-1. Klonen das Repository:
- `git clone https://github.com/GalacticCodeGambit/LazyCook.git`
-2. Navigieren zum Projektverzeichnis:
- `cd LazyCook/Project`
-3. Starten der Anwendung mit: Docker Compose:
- `docker compose up --build -d`
-
-Die Anwendung sollte jetzt unter `http://localhost:8000` erreichbar sein.
+## Entwicklung
+### Linter
+#### Python `Black`
+Automatisch formatieren:
+```
+black project/backend/ .
+```
+
+`Black` installieren:
+```
+python -m pip install black
+```
-## Probleme beim Entwickeln
+### Probleme beim Entwickeln
Problem: Code hinzugefügt/geändert aber Änderungen werden nicht übernommen von Docker
```
From 7f7f042042b51fe7e1edb3851735fb5b9e41d9da Mon Sep 17 00:00:00 2001
From: GalacticCodeGambit
<150372421+GalacticCodeGambit@users.noreply.github.com>
Date: Fri, 8 May 2026 15:21:19 +0200
Subject: [PATCH 14/85] Update Node.js version in Dockerfile-frontend to 24.1.0
---
project/Dockerfile-frontend | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/project/Dockerfile-frontend b/project/Dockerfile-frontend
index fc2b0e3..b3ee918 100644
--- a/project/Dockerfile-frontend
+++ b/project/Dockerfile-frontend
@@ -1,4 +1,4 @@
-FROM node:24-alpine3.20
+FROM node:24.1.0-alpine3.20
WORKDIR /app
# Copy package.json and package-lock.json
From 64be75daf053de761c57e311535cf34f4b52f6a6 Mon Sep 17 00:00:00 2001
From: PrussianBaron
Date: Sat, 9 May 2026 14:52:39 +0200
Subject: [PATCH 15/85] Tested the recipe algorithim
---
project/backend/Database.py | 155 ++++++++++++++++--
.../backend/{Ingridient.py => Ingredient.py} | 2 +-
project/backend/Recipe.py | 34 ++--
project/backend/RecipeSucuk.py | 48 ++++--
4 files changed, 187 insertions(+), 52 deletions(-)
rename project/backend/{Ingridient.py => Ingredient.py} (95%)
diff --git a/project/backend/Database.py b/project/backend/Database.py
index 8736849..ae1621b 100644
--- a/project/backend/Database.py
+++ b/project/backend/Database.py
@@ -3,6 +3,7 @@
from pathlib import Path
import os
+
# Verwende die Datenbank aus dem data-Ordner
DB_PATH = Path(__file__).parent.parent / "data" / "LazyCookDB.sqlite3"
@@ -61,7 +62,7 @@ def initDB():
""")
cur.execute("""
- CREATE TABLE IF NOT EXISTS Ingridient (
+ CREATE TABLE IF NOT EXISTS Ingredient (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
amountType VARCHAR(30) NOT NULL
@@ -79,8 +80,8 @@ def initDB():
CREATE TABLE IF NOT EXISTS Recipe (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
- description nvarchar(MAX),
- vid INTEGER NOT NULL,
+ description nvarchar(20000),
+ vid INTEGER,
FOREIGN KEY (vid) REFERENCES Author (id) ON DELETE CASCADE
)
""")
@@ -90,7 +91,7 @@ def initDB():
zid INTEGER NOT NULL,
rid INTEGER NOT NULL,
amount DECIMAL(10,2) NOT NULL,
- FOREIGN KEY (zid) REFERENCES Ingridient (id) ON DELETE CASCADE,
+ FOREIGN KEY (zid) REFERENCES Ingredient (id) ON DELETE CASCADE,
FOREIGN KEY (rid) REFERENCES Recipe (id) ON DELETE CASCADE,
UNIQUE (zid, rid)
)
@@ -211,6 +212,33 @@ def cleanupExpiredTokens() -> None:
with getDB() as con:
cur = con.cursor()
cur.execute("DELETE FROM RefreshToken WHERE expiresAt < datetime('now')")
+
+def addRecipe(name: str, description: str, vid: int) -> bool:
+ with getDB() as con:
+ cur = con.cursor()
+ cur.execute(
+ "INSERT INTO Recipe (name, description) VALUES (?, ?)",
+ (name, description),
+ )
+ return cur.lastrowid
+
+def addIngredientToRecipe(zid: int, rid: int, amount: float) -> bool:
+ with getDB() as con:
+ cur = con.cursor()
+ cur.execute(
+ "INSERT INTO Exists_from (zid, rid, amount) VALUES (?, ?, ?)",
+ (zid, rid, amount),
+ )
+
+def addIngredient(name: str, amountType: str) -> bool:
+ with getDB() as con:
+ cur = con.cursor()
+ cur.execute(
+ "INSERT INTO Ingredient (name, amountType) VALUES (?, ?)",
+ (name, amountType),
+ )
+ return cur.lastrowid
+
def getRecipe(recipeID: int) -> dict | None:
"""Gibt eine Liste aller Rezepte zurück."""
@@ -220,7 +248,7 @@ def getRecipe(recipeID: int) -> dict | None:
SELECT name, description
FROM Recipe
WHERE id = ?
- """, (recipeID,))
+ """, (recipeID))
row = cur.fetchone()
return dict(row) if row else None
@@ -230,21 +258,22 @@ def getAllRecipes()-> list[dict]:
cur.execute("""
SELECT id, name, description
FROM Recipe
-
""")
rows = cur.fetchall()
return [dict(row) for row in rows]
-def getAllIngridientsForRecipe(id: int) -> list[dict]:
- """Gibt eine Liste aller Zutaten zurück."""
+def getAllIngredientsForRecipe(rid: int):
with getDB() as con:
cur = con.cursor()
cur.execute("""
- SELECT id, name, Exists_from.amount
- FROM Ingridient
- Inner Join Exists_from id = zid
- Inner Join Recipe rid = id
- Where id = ?
- """, (id,))
+ SELECT
+ Ingredient.name,
+ Exists_from.amount,
+ Ingredient.amountType
+ FROM Ingredient
+ JOIN Exists_from ON Ingredient.id = Exists_from.zid
+ JOIN Recipe ON Exists_from.rid = Recipe.id
+ WHERE Exists_from.rid = ?
+ """, (rid,)) # Note the comma here!
rows = cur.fetchall()
return [dict(row) for row in rows]
@@ -255,9 +284,103 @@ def getAllocatedRecipes(name: str)-> list[dict]:
cur.execute("""
SELECT rid
FROM Exists_from
- Inner Join Ingridient on rid=id,
- Where id = ?
+ Inner Join Ingredient on rid=id,
+ Where Exists_from.id = ?
""", (name,))
rows = cur.fetchall()
return [dict(row) for row in rows]
+initDB()
+"""
+# --- 1. Fresh Pesto Pasta ---
+r1 = addRecipe("Fresh Pesto Pasta", "Classic Italian basil pesto with linguine.", 301)
+i1 = addIngredient("Linguine", "grams")
+i2 = addIngredient("Fresh Basil", "grams")
+i3 = addIngredient("Pine Nuts", "grams")
+i4 = addIngredient("Parmesan", "grams")
+addIngredientToRecipe(i1, r1, 200.0)
+addIngredientToRecipe(i2, r1, 50.0)
+addIngredientToRecipe(i3, r1, 30.0)
+addIngredientToRecipe(i4, r1, 40.0)
+
+# --- 2. Beef Tacos ---
+r2 = addRecipe("Beef Tacos", "Street-style seasoned beef tacos.", 302)
+i5 = addIngredient("Corn Tortillas", "pieces")
+i6 = addIngredient("Ground Beef", "grams")
+i7 = addIngredient("Cumin", "teaspoons")
+addIngredientToRecipe(i5, r2, 3.0)
+addIngredientToRecipe(i6, r2, 150.0)
+addIngredientToRecipe(i7, r2, 1.0)
+
+# --- 3. Greek Salad ---
+r3 = addRecipe("Greek Salad", "Refreshing cucumber and feta salad.", 303)
+i8 = addIngredient("Cucumber", "pieces")
+i9 = addIngredient("Feta Cheese", "grams")
+i10 = addIngredient("Kalamata Olives", "pieces")
+addIngredientToRecipe(i8, r3, 1.0)
+addIngredientToRecipe(i9, r3, 100.0)
+addIngredientToRecipe(i10, r3, 10.0)
+
+# --- 4. Mushroom Risotto ---
+r4 = addRecipe("Mushroom Risotto", "Creamy arborio rice with wild mushrooms.", 304)
+i11 = addIngredient("Arborio Rice", "grams")
+i12 = addIngredient("Mushrooms", "grams")
+i13 = addIngredient("Vegetable Broth", "ml")
+addIngredientToRecipe(i11, r4, 150.0)
+addIngredientToRecipe(i12, r4, 200.0)
+addIngredientToRecipe(i13, r4, 500.0)
+
+# --- 5. French Toast ---
+r5 = addRecipe("French Toast", "Brioche soaked in cinnamon egg wash.", 305)
+i14 = addIngredient("Brioche Bread", "slices")
+i15 = addIngredient("Cinnamon", "teaspoons")
+# Reusing 'Egg' and 'Milk' from previous datasets if they exist
+addIngredientToRecipe(14, r5, 2.0) # Brioche
+addIngredientToRecipe(2, r5, 2.0) # Egg (zid 2)
+addIngredientToRecipe(15, r5, 0.5) # Cinnamon
+
+# --- 6. Margherita Pizza ---
+r6 = addRecipe("Margherita Pizza", "Simple pizza with tomato, mozzarella, and basil.", 306)
+i16 = addIngredient("Pizza Dough", "grams")
+i17 = addIngredient("Mozzarella", "grams")
+addIngredientToRecipe(i16, r6, 250.0)
+addIngredientToRecipe(i17, r6, 120.0)
+addIngredientToRecipe(i2, r6, 10.0) # Basil
+
+# --- 7. Salmon with Asparagus ---
+r7 = addRecipe("Baked Salmon", "Lemon-butter salmon with roasted asparagus.", 307)
+i18 = addIngredient("Salmon Fillet", "grams")
+i19 = addIngredient("Asparagus", "grams")
+i20 = addIngredient("Lemon", "pieces")
+addIngredientToRecipe(i18, r7, 200.0)
+addIngredientToRecipe(i19, r7, 150.0)
+addIngredientToRecipe(i20, r7, 0.5)
+
+# --- 8. Chicken Stir-fry ---
+r8 = addRecipe("Chicken Stir-fry", "Quick soy-ginger chicken and veggies.", 308)
+i21 = addIngredient("Soy Sauce", "ml")
+i22 = addIngredient("Ginger", "grams")
+i23 = addIngredient("Broccoli", "grams")
+addIngredientToRecipe(5, r8, 200.0) # Chicken Breast (zid 5)
+addIngredientToRecipe(i21, r8, 30.0)
+addIngredientToRecipe(i22, r8, 10.0)
+addIngredientToRecipe(i23, r8, 100.0)
+
+# --- 9. Guacamole ---
+r9 = addRecipe("Guacamole", "Chunky avocado dip with lime.", 309)
+i24 = addIngredient("Avocado", "pieces")
+i25 = addIngredient("Lime Juice", "ml")
+addIngredientToRecipe(i24, r9, 3.0)
+addIngredientToRecipe(i25, r9, 15.0)
+addIngredientToRecipe(4, r9, 0.25) # Salt
+
+# --- 10. Chocolate Chip Cookies ---
+r10 = addRecipe("Chocolate Cookies", "Chewy cookies with dark chocolate chips.", 310)
+i26 = addIngredient("Chocolate Chips", "grams")
+i27 = addIngredient("Vanilla Extract", "ml")
+addIngredientToRecipe(1, r10, 250.0) # Flour
+addIngredientToRecipe(14, r10, 150.0) # Butter
+addIngredientToRecipe(11, r10, 100.0) # Sugar
+addIngredientToRecipe(i26, r10, 100.0)
+addIngredientToRecipe(i27, r10, 5.0)
+"""
\ No newline at end of file
diff --git a/project/backend/Ingridient.py b/project/backend/Ingredient.py
similarity index 95%
rename from project/backend/Ingridient.py
rename to project/backend/Ingredient.py
index 232b9f3..c0ad485 100644
--- a/project/backend/Ingridient.py
+++ b/project/backend/Ingredient.py
@@ -1,7 +1,7 @@
from typing import List, Dict, Any
-class Ingridient:
+class Ingredient:
def __init__(self, name: str, amount: float):
self.__name = name
self.__amount = amount
diff --git a/project/backend/Recipe.py b/project/backend/Recipe.py
index 788664c..14721e4 100644
--- a/project/backend/Recipe.py
+++ b/project/backend/Recipe.py
@@ -1,39 +1,39 @@
-from Database import Database
-from Ingridient import Ingridient
+
+from Ingredient import Ingredient
class Recipe:
- def __init__(self, name: str, ingridients: list[Ingridient], description: str):
+ def __init__(self, name: str, Ingredients: list[Ingredient], description: str):
self.__name = name
- self.__Ingridients = ingridients
+ self.__Ingredients = Ingredients
self.__description = description
self.__original = ""
self.__duration = ""
self.__rating = 0.0
self.__countPersons = 1
self.__matching = 0
- self.__database = Database()
+
def saveInDB(self) -> bool:
return True
def getName(self) -> str:
- return self.name
+ return self.__name
def setName(self, name: str):
- self.name = name
+ self.__name = name
def getOriginal(self) -> str:
- return self.original
+ return self.__original
def setOriginal(self, original: str):
- self.original = original
+ self.__original = original
def getDescription(self) -> str:
- return self.description
+ return self.__description
def setDescription(self, description: str):
- self.description = description
+ self.__description = description
def getRating(self) -> float:
return self.__rating
@@ -48,14 +48,14 @@ def incrementMatching(self):
self.__matching+=1
def getDuration(self) -> str:
- return self.duration
+ return self.__duration
def setDuration(self, duration: str):
- self.duration = duration
+ self.__duration = duration
- def getIngridients(self) -> list[Ingridient]:
- return self.__ingridients
+ def getIngredients(self) -> list[Ingredient]:
+ return self.__Ingredients
- def setIngridient(self, ingridients: list[Ingridient]):
- self.__Ingridients = ingridients
+ def setIngredient(self, Ingredients: list[Ingredient]):
+ self.__Ingredients = Ingredients
diff --git a/project/backend/RecipeSucuk.py b/project/backend/RecipeSucuk.py
index 25d1e0c..64aafc3 100644
--- a/project/backend/RecipeSucuk.py
+++ b/project/backend/RecipeSucuk.py
@@ -1,42 +1,54 @@
import Database
-from Ingridient import Ingridient
+from Ingredient import Ingredient
from Recipe import Recipe
-from Database import getAllRecipes, getAllIngridientsForRecipe
+from Database import getAllRecipes, getAllIngredientsForRecipe
-def findRecipes(ingriedents: list[Ingridient])-> list[Recipe]:
+def findRecipes(ingriedents: list[Ingredient])-> list[Recipe]:
recipes = __initRecipes()
- for ingridient in ingriedents:
- recipes = __filterRecipes(recipes)
+ for Ingredient in ingriedents:
+ recipes = __filterRecipes(recipes, Ingredient)
recipes.sort(key=lambda x: x.getRating(), reverse=True)
for recipe in recipes[::-1]:
- if recipe.getMatching < 3:
+ if recipe.getRating() < 0.3:
recipes.remove(recipe)
else:
break
return recipes
-def __filterRecipes(recipes: list[Recipe], ingridient: Ingridient)-> list[Recipe]:
+def __filterRecipes(recipes: list[Recipe], Ingredient: Ingredient)-> list[Recipe]:
for recipe in recipes:
- for recipeIngridient in recipe.getIngridients():
- if recipeIngridient.getName() == ingridient.getName():
+ for recipeIngredient in recipe.getIngredients():
+ if recipeIngredient.getName() == Ingredient.getName():
recipe.incrementMatching()
- recipe.setRating(recipe.getMatching/ len(recipe.getIngridients))
+ recipe.setRating(recipe.getMatching()/ len(recipe.getIngredients()))
return recipes
def __initRecipes()-> list[Recipe]:
- recipesRaw = getAllRecipes
+ recipesRaw = getAllRecipes()
recipes = []
for recipeRaw in recipesRaw:
- recipe = Recipe(recipeRaw["name"], __formatIngridients(recipeRaw["id"]), recipeRaw["description"])
+ recipe = Recipe(recipeRaw["name"], __formatIngredients(recipeRaw["id"]), recipeRaw["description"])
recipes.append(recipe)
return recipes
-def __formatIngridients(id: int)-> list[Ingridient]:
- ingridientsRaw = getAllIngridientsForRecipe(id)
- ingridients = []
- for ingridientRaw in ingridientsRaw:
- ingridients.append(Ingridient(ingridientRaw["name"],ingridientRaw["amount"]))
- return ingridients
\ No newline at end of file
+def __formatIngredients(id: int)-> list[Ingredient]:
+ IngredientsRaw = getAllIngredientsForRecipe(id)
+ Ingredients = []
+ for IngredientRaw in IngredientsRaw:
+ Ingredients.append(Ingredient(IngredientRaw["name"],IngredientRaw["amount"]))
+ return Ingredients
+
+ini = []
+ini.append(Ingredient("Linguine", 10 ))
+ini.append(Ingredient("Fresh Basil", 10 ))
+ini.append(Ingredient("Pine Nuts", 10 ))
+ini.append(Ingredient("Parmesan", 10 ))
+ini.append(Ingredient("Ground Beef", 10 ))
+ini.append(Ingredient("Cumin", 10 ))
+ini.append(Ingredient("Cucumber", 10 ))
+arr = findRecipes(ini)
+for i in arr:
+ print(i.getName()+ " "+ str(i.getRating()))
\ No newline at end of file
From fb332478664ba1485c5c192df8bc48453d804b1b Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Sat, 9 May 2026 16:02:54 +0200
Subject: [PATCH 16/85] Feature: Fix problem with refresh token and include
show Password
---
project/backend/Models.py | 1 +
project/backend/Routes.py | 2 +-
project/frontend/app/components/fields.tsx | 50 ++++++++++++---
.../app/profile/changePasswordPopup.tsx | 62 +++++++++++++++----
project/frontend/lib/auth.tsx | 26 ++++----
5 files changed, 106 insertions(+), 35 deletions(-)
diff --git a/project/backend/Models.py b/project/backend/Models.py
index 86638ac..5ccd306 100644
--- a/project/backend/Models.py
+++ b/project/backend/Models.py
@@ -3,6 +3,7 @@
class Token(BaseModel):
access_token: str
+ refresh_token: str
token_type: str
class User(BaseModel):
diff --git a/project/backend/Routes.py b/project/backend/Routes.py
index 1c9a077..570dda8 100644
--- a/project/backend/Routes.py
+++ b/project/backend/Routes.py
@@ -129,7 +129,7 @@ async def updateCurrentUser(
# Passwort ändern
if data.currentPassword and data.newPassword:
if not verifyPassword(data.currentPassword, Account["hashedPassword"]):
- raise HTTPException(status_code=401, detail="Falsches Passwort")
+ raise HTTPException(status_code=400, detail="Aktuelles Passwort ist falsch")
error = validatePassword(data.newPassword)
if error:
diff --git a/project/frontend/app/components/fields.tsx b/project/frontend/app/components/fields.tsx
index 9e3fdae..52ad3a4 100644
--- a/project/frontend/app/components/fields.tsx
+++ b/project/frontend/app/components/fields.tsx
@@ -1,3 +1,5 @@
+import { useState } from "react";
+import { Eye, EyeOff } from "lucide-react";
import styles from "../homepage/page.module.css";
export default function Field({ label, type = "text", value, onChange, placeholder, onKeyDown, onBlur, state = "default" }: {
@@ -7,6 +9,10 @@ export default function Field({ label, type = "text", value, onChange, placehold
onBlur?: () => void;
state?: "default" | "error" | "success";
}) {
+ const [showPassword, setShowPassword] = useState(false);
+ const isPassword = type === "password";
+ const effectiveType = isPassword && showPassword ? "text" : type;
+
const inputClass =
state === "error" ? styles.inputError :
state === "success" ? styles.inputSuccess :
@@ -15,15 +21,41 @@ export default function Field({ label, type = "text", value, onChange, placehold
return (
-
onChange(e.target.value)}
- placeholder={placeholder}
- onKeyDown={onKeyDown}
- onBlur={onBlur}
- />
+
+ onChange(e.target.value)}
+ placeholder={placeholder}
+ onKeyDown={onKeyDown}
+ onBlur={onBlur}
+ style={isPassword ? { paddingRight: 38 } : undefined}
+ />
+ {isPassword && (
+
+ )}
+
);
}
\ No newline at end of file
diff --git a/project/frontend/app/profile/changePasswordPopup.tsx b/project/frontend/app/profile/changePasswordPopup.tsx
index 882669b..2e05d2c 100644
--- a/project/frontend/app/profile/changePasswordPopup.tsx
+++ b/project/frontend/app/profile/changePasswordPopup.tsx
@@ -1,8 +1,50 @@
import {fetchWithAuth} from "@/lib/auth";
import {useState} from "react";
import {Button} from "@/app/components/ui/button";
+import {Eye, EyeOff} from "lucide-react";
import "../recipeFinder/style.css"
+function PasswordInput({ value, onChange, placeholder }: {
+ value: string;
+ onChange: (v: string) => void;
+ placeholder: string;
+}) {
+ const [show, setShow] = useState(false);
+ return (
+
+ onChange(e.target.value)}
+ className="popup__input"
+ style={{ width: "100%", paddingRight: 38 }}
+ />
+
+
+ );
+}
+
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";
type Modus = "change" | "forgot";
@@ -63,7 +105,7 @@ export default function ChangePassword({ modus, onSuccess }: ChangePasswordProps
setConfirmPassword("");
onSuccess?.();
} catch {
- setPasswordMsg("Unbekannter Fehler.");
+ setPasswordMsg('❌ Unbekannter Fehler');
}
}
@@ -75,29 +117,23 @@ export default function ChangePassword({ modus, onSuccess }: ChangePasswordProps
{!isForgot && (
- setCurrentPassword(e.target.value)}
- className="popup__input"
+ onChange={setCurrentPassword}
/>
)}
- setNewPassword(e.target.value)}
- className="popup__input"
+ onChange={setNewPassword}
/>
- setConfirmPassword(e.target.value)}
- className="popup__input"
+ onChange={setConfirmPassword}
/>
diff --git a/project/frontend/lib/auth.tsx b/project/frontend/lib/auth.tsx
index cae660e..2f151dd 100644
--- a/project/frontend/lib/auth.tsx
+++ b/project/frontend/lib/auth.tsx
@@ -39,6 +39,9 @@ function getRefreshToken(): string | null {
}
function saveTokens(accessToken: string, refreshToken: string) {
+ if (!accessToken || !refreshToken) {
+ throw new Error("Tokens fehlen — Backend-Response unvollständig");
+ }
sessionStorage.setItem("access_token", accessToken);
localStorage.setItem("refresh_token", refreshToken);
}
@@ -118,23 +121,22 @@ async function fetchWithAuth(url: string, options: RequestInit = {}): Promise
Date: Sun, 10 May 2026 14:48:34 +0200
Subject: [PATCH 17/85] Feature: forgot password functionality works with
redirection to homepage
---
project/backend/Auth.py | 4 +-
project/backend/Database.py | 23 +++++++++--
project/backend/EmailService.py | 40 +++++++++++++++++++
project/backend/LazyCookAdministration.py | 6 ++-
project/backend/Routes.py | 30 ++++++++++----
project/compose.yaml | 1 +
.../frontend/app/homepage/forgotPassword.tsx | 1 +
.../frontend/app/profile/changeEmailPopup.tsx | 1 +
.../app/profile/changePasswordPopup.tsx | 20 +++++-----
project/frontend/app/reset-password/page.tsx | 2 +
project/frontend/lib/auth.tsx | 3 +-
11 files changed, 106 insertions(+), 25 deletions(-)
diff --git a/project/backend/Auth.py b/project/backend/Auth.py
index 1ffde5d..9e89313 100644
--- a/project/backend/Auth.py
+++ b/project/backend/Auth.py
@@ -140,9 +140,9 @@ def createPasswordResetToken(kontoId: int) -> str:
def validatePasswordResetToken(token: str) -> dict | None:
from Database import getPasswordResetToken
entry = getPasswordResetToken(hashResetToken(token))
- if entry is None or entry["used_at"] is not None:
+ if entry is None or entry["usedAt"] is not None:
return None
- expiresAt = datetime.fromisoformat(entry["expires_at"])
+ expiresAt = datetime.fromisoformat(entry["expiresAt"])
if expiresAt.tzinfo is None:
expiresAt = expiresAt.replace(tzinfo=timezone.utc)
if expiresAt < datetime.now(timezone.utc):
diff --git a/project/backend/Database.py b/project/backend/Database.py
index 4601e4c..173b898 100644
--- a/project/backend/Database.py
+++ b/project/backend/Database.py
@@ -226,7 +226,7 @@ def savePasswordResetToken(kontoID: int, tokenHash: str, expiresAt: str) -> None
cur = con.cursor()
# Alte, ungenutzte Tokens für dieses Konto invalidieren
cur.execute(
- "UPDATE PasswordResetToken SET used_at = CURRENT_TIMESTAMP "
+ "UPDATE PasswordResetToken SET usedAt = CURRENT_TIMESTAMP "
"WHERE kontoID = ? AND usedAt IS NULL",
(kontoID,),
)
@@ -256,7 +256,7 @@ def markResetTokenUsed(tokenID: int) -> None:
cur = con.cursor()
cur.execute(
"UPDATE PasswordResetToken SET usedAt = CURRENT_TIMESTAMP WHERE id = ?",
- (token_id,),
+ (tokenID,),
)
@@ -264,6 +264,21 @@ def updateKontoPassword(konto_id: int, hashed_password: str) -> None:
with getDB() as con:
cur = con.cursor()
cur.execute(
- "UPDATE Account SET hashed_password = ? WHERE id = ?",
+ "UPDATE Account SET hashedPassword = ? WHERE id = ?",
(hashed_password, konto_id),
- )
\ No newline at end of file
+ )
+
+
+def getAccountById(konto_id: int) -> dict | None:
+ """Gibt Account-Daten anhand der ID zurück, oder None."""
+ con = getConnection()
+ try:
+ cur = con.cursor()
+ cur.execute(
+ "SELECT id, email, name, hashedPassword FROM Account WHERE id = ?",
+ (konto_id,),
+ )
+ row = cur.fetchone()
+ return dict(row) if row else None
+ finally:
+ con.close()
\ No newline at end of file
diff --git a/project/backend/EmailService.py b/project/backend/EmailService.py
index caa8a0f..67506b5 100644
--- a/project/backend/EmailService.py
+++ b/project/backend/EmailService.py
@@ -28,6 +28,46 @@ def sendPasswordChangedEmail(to_email: str, name: str) -> None:
server.login(gmailUser, gmailPassword)
server.sendmail(gmailUser, to_email, msg.as_string())
+ except Exception as e:
+ print(f"E-Mail Fehler: {e}")
+ raise
+
+
+def sendPasswordResetEmail(to_email: str, name: str, resetLink: str) -> None:
+ try:
+ msg = MIMEMultipart("alternative")
+ msg["Subject"] = "Passwort zurücksetzen – Lazy Cook"
+ msg["From"] = gmailUser
+ msg["To"] = to_email
+
+ html = f"""
+
+
Hallo {name},
+
du hast angefordert, dein Passwort bei Lazy Cook zurückzusetzen.
+
Klicke auf den Button, um ein neues Passwort festzulegen:
+
+
+ Passwort zurücksetzen
+
+
+
+ Oder kopiere diesen Link in deinen Browser:
+ {resetLink}
+
+
Der Link ist 30 Minuten gültig.
+
Falls du das nicht angefordert hast, ignoriere diese E-Mail einfach – dein Passwort bleibt unverändert.
+
+
– Das Lazy Cook Team
+
+ """
+ msg.attach(MIMEText(html, "html"))
+
+ with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
+ server.login(gmailUser, gmailPassword)
+ server.sendmail(gmailUser, to_email, msg.as_string())
+
except Exception as e:
print(f"E-Mail Fehler: {e}")
raise
\ No newline at end of file
diff --git a/project/backend/LazyCookAdministration.py b/project/backend/LazyCookAdministration.py
index 8f7df0d..9750ae0 100644
--- a/project/backend/LazyCookAdministration.py
+++ b/project/backend/LazyCookAdministration.py
@@ -1,3 +1,4 @@
+import os
from contextlib import asynccontextmanager
from fastapi import FastAPI
@@ -6,6 +7,9 @@
from Database import initDB
from Routes import router
+# Frontend-URL aus Env, mit Dev-Fallback (Frontend läuft per compose.yaml auf Port 8000)
+FRONTEND_URL = os.environ.get("FRONTEND_URL", "http://localhost:8000")
+
@asynccontextmanager
async def lifespan(app: FastAPI):
@@ -17,7 +21,7 @@ async def lifespan(app: FastAPI):
app.add_middleware(
CORSMiddleware,
- allow_origins=["http://localhost:8000"],
+ allow_origins=[FRONTEND_URL],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
diff --git a/project/backend/Routes.py b/project/backend/Routes.py
index 570dda8..7f28ef7 100644
--- a/project/backend/Routes.py
+++ b/project/backend/Routes.py
@@ -1,7 +1,8 @@
+import os
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
-from EmailService import sendPasswordChangedEmail
+from EmailService import sendPasswordChangedEmail, sendPasswordResetEmail
from pydantic import BaseModel as _BaseModel
@@ -20,6 +21,7 @@
from Database import (
createAccount,
getAccountByEmail,
+ getAccountById,
deleteRefreshToken,
deleteAllRefreshTokens,
deleteAccount,
@@ -32,6 +34,9 @@
from Models import User, Token, UserCreate, RefreshRequest, LogoutRequest, ForgotPasswordRequest, ResetPasswordRequest, UpdateUser
+# Frontend-URL aus Env, mit Dev-Fallback (Frontend läuft per compose.yaml auf Port 8000)
+FRONTEND_URL = os.environ.get("FRONTEND_URL", "http://localhost:8000")
+
router = APIRouter()
@@ -84,7 +89,7 @@ async def refresh(body: RefreshRequest):
deleteRefreshToken(body.refresh_token)
# Account-Daten für neues Token-Paar zusammenstellen
- Account = {"id": entry["konto_id"], "email": entry["email"]}
+ Account = {"id": entry["AccountID"], "email": entry["email"]}
return createTokenPair(Account)
@@ -157,9 +162,12 @@ async def forgotPassword(body: ForgotPasswordRequest):
# Token nur erzeugen wenn Konto existiert – aber IMMER gleiche Antwort senden!
if konto is not None:
token = createPasswordResetToken(konto["id"])
- resetLink = f"http://localhost:8000/reset-password?token={token}"
- # TODO: Mail versenden – siehe Hinweis unten
- print(f"[DEV] Reset-Link für {body.email}: {resetLink}")
+ resetLink = f"{FRONTEND_URL}/reset-password?token={token}"
+ try:
+ sendPasswordResetEmail(body.email, konto["name"], resetLink)
+ except Exception as e:
+ # Mail-Fehler darf das Response nicht beeinflussen → User-Enumeration vermeiden
+ print(f"Reset-Mail konnte nicht gesendet werden: {e}")
return {"detail": "Falls die E-Mail existiert, wurde ein Link versendet."}
@@ -178,9 +186,17 @@ async def resetPassword(body: ResetPasswordRequest):
detail="Link ungültig oder abgelaufen. Bitte neuen anfordern.",
)
- updateKontoPassword(entry["konto_id"], hashPassword(body.new_password))
+ updateKontoPassword(entry["kontoID"], hashPassword(body.new_password))
markResetTokenUsed(entry["id"])
# Sicherheitsmaßnahme: Alle aktiven Sessions ungültig machen
- deleteAllRefreshTokens(entry["konto_id"])
+ deleteAllRefreshTokens(entry["kontoID"])
+
+ # Bestätigungsmail an den Konto-Inhaber
+ konto = getAccountById(entry["kontoID"])
+ if konto is not None:
+ try:
+ sendPasswordChangedEmail(konto["email"], konto["name"])
+ except Exception as e:
+ print(f"Bestätigungsmail konnte nicht gesendet werden: {e}")
return {"detail": "Passwort erfolgreich zurückgesetzt."}
diff --git a/project/compose.yaml b/project/compose.yaml
index 6bffc1c..c63814c 100644
--- a/project/compose.yaml
+++ b/project/compose.yaml
@@ -17,6 +17,7 @@ services:
- PYTHONUNBUFFERED=1
- GMAIL_USER=${GMAIL_USER}
- GMAIL_PASSWORD=${GMAIL_PASSWORD}
+ - FRONTEND_URL=${FRONTEND_URL:-http://localhost:8000}
volumes:
- ./data:/data
ports:
diff --git a/project/frontend/app/homepage/forgotPassword.tsx b/project/frontend/app/homepage/forgotPassword.tsx
index cd778d1..0186cac 100644
--- a/project/frontend/app/homepage/forgotPassword.tsx
+++ b/project/frontend/app/homepage/forgotPassword.tsx
@@ -57,6 +57,7 @@ export default function ForgotPasswordForm({ onClose, onBack,}: { onClose: () =>
onChange={(v: string) => setEmail(v)}
placeholder="Email"
onBlur={() => setEmailBlurred(true)}
+ onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
state={
emailBlurred && !emailValid ? "error" : emailValid ? "success" : "default"
}
diff --git a/project/frontend/app/profile/changeEmailPopup.tsx b/project/frontend/app/profile/changeEmailPopup.tsx
index cd6045e..1d1dd3b 100644
--- a/project/frontend/app/profile/changeEmailPopup.tsx
+++ b/project/frontend/app/profile/changeEmailPopup.tsx
@@ -39,6 +39,7 @@ export default function ChangeEmail (){
placeholder="Neue E-Mail-Adresse"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
+ onKeyDown={(e) => e.key === "Enter" && handleEmailChange()}
className="popup__input"
/>
diff --git a/project/frontend/app/profile/changePasswordPopup.tsx b/project/frontend/app/profile/changePasswordPopup.tsx
index 2e05d2c..7105c6b 100644
--- a/project/frontend/app/profile/changePasswordPopup.tsx
+++ b/project/frontend/app/profile/changePasswordPopup.tsx
@@ -3,11 +3,15 @@ import {useState} from "react";
import {Button} from "@/app/components/ui/button";
import {Eye, EyeOff} from "lucide-react";
import "../recipeFinder/style.css"
+import Field from "@/app/components/fields";
-function PasswordInput({ value, onChange, placeholder }: {
+const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";
+
+function PasswordInput({ value, onChange, placeholder, onKeyDown }: {
value: string;
onChange: (v: string) => void;
placeholder: string;
+ onKeyDown?: (e: React.KeyboardEvent) => void;
}) {
const [show, setShow] = useState(false);
return (
@@ -17,6 +21,7 @@ function PasswordInput({ value, onChange, placeholder }: {
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
+ onKeyDown={onKeyDown}
className="popup__input"
style={{ width: "100%", paddingRight: 38 }}
/>
@@ -45,8 +50,6 @@ function PasswordInput({ value, onChange, placeholder }: {
);
}
-const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";
-
type Modus = "change" | "forgot";
interface ChangePasswordProps {
@@ -60,6 +63,7 @@ export default function ChangePassword({ modus, onSuccess }: ChangePasswordProps
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [passwordMsg, setPasswordMsg] = useState("");
+ const [pwBlurred, setPwBlurred] = useState(false);
const isForgot = modus === "forgot";
@@ -117,23 +121,21 @@ export default function ChangePassword({ modus, onSuccess }: ChangePasswordProps
{!isForgot && (
-
- )}
+
setPwBlurred(true)} onKeyDown={(e) => e.key === "Enter" && handlePasswordChange()} state={pwBlurred && !currentPassword? "error" : "default"} />)}
+
e.key === "Enter" && handlePasswordChange()}
/>
e.key === "Enter" && handlePasswordChange()}
/>
diff --git a/project/frontend/app/reset-password/page.tsx b/project/frontend/app/reset-password/page.tsx
index a56795c..aacc4d0 100644
--- a/project/frontend/app/reset-password/page.tsx
+++ b/project/frontend/app/reset-password/page.tsx
@@ -90,6 +90,7 @@ function ResetPasswordContent() {
value={password}
onChange={setPassword}
placeholder="••••••••"
+ onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
state={password && !pwValid ? "error" : "default"}
/>
e.key === "Enter" && handleSubmit()}
state={confirm && !pwMatch ? "error" : pwMatch ? "success" : "default"}
/>
+ {error && {error}
}
void;
placeholder: string;
onKeyDown?: (e: React.KeyboardEvent) => void;
+ ariaLabel?: string;
}) {
const [show, setShow] = useState(false);
return (
@@ -19,6 +20,7 @@ function PasswordInput({ value, onChange, placeholder, onKeyDown }: {
onChange(e.target.value)}
onKeyDown={onKeyDown}
@@ -29,7 +31,7 @@ function PasswordInput({ value, onChange, placeholder, onKeyDown }: {
type="button"
onClick={() => setShow((s) => !s)}
aria-label={show ? "Passwort verbergen" : "Passwort anzeigen"}
- tabIndex={-1}
+ aria-pressed={show}
style={{
position: "absolute",
right: 10,
@@ -70,7 +72,21 @@ export default function ChangePassword({ modus, onSuccess }: ChangePasswordProps
async function handlePasswordChange() {
setPasswordMsg("");
- // Bestätigung prüfen (nur im forgot-Modus relevant, aber sinnvoll auch beim Ändern)
+ // Pflichtfelder im "change"-Modus
+ if (!isForgot && !currentPassword) {
+ setPasswordMsg("Bitte das aktuelle Passwort eingeben.");
+ return;
+ }
+ if (!newPassword) {
+ setPasswordMsg("Bitte ein neues Passwort eingeben.");
+ return;
+ }
+ if (!confirmPassword) {
+ setPasswordMsg("Bitte das neue Passwort bestätigen.");
+ return;
+ }
+
+ // Bestätigung prüfen
if (newPassword !== confirmPassword) {
setPasswordMsg("Die Passwörter stimmen nicht überein.");
return;
diff --git a/project/frontend/app/profile/page.tsx b/project/frontend/app/profile/page.tsx
index d4ac84f..a6be68f 100644
--- a/project/frontend/app/profile/page.tsx
+++ b/project/frontend/app/profile/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import {useEffect, useRef, useState} from "react";
+import {useEffect, useState} from "react";
import { useAuth, fetchWithAuth } from "@/lib/auth";
import { useRouter } from "next/navigation";
import {ChefHat} from "lucide-react";
@@ -17,7 +17,6 @@ const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";
export default function Profile() {
const { user, loading, logout } = useAuth();
const router = useRouter();
- const menuRef = useRef(null);
const [showConfirm, setShowConfirm] = useState(false);
@@ -94,7 +93,7 @@ export default function Profile() {
setShowConfirm(true)}
+ onClick={() => setShowConfirm (true)}
>
Konto löschen
diff --git a/project/frontend/app/recipeFinder/page.tsx b/project/frontend/app/recipeFinder/page.tsx
index a0a276b..458c09f 100644
--- a/project/frontend/app/recipeFinder/page.tsx
+++ b/project/frontend/app/recipeFinder/page.tsx
@@ -3,7 +3,7 @@
import {useEffect, useRef, useState} from "react";
import { useRouter } from "next/navigation";
import {fetchWithAuth, useAuth} from "@/lib/auth";
-import {ChefHat, LogOut, X, User, UserCircle, Search, Plus} from "lucide-react";
+import {ChefHat, X, Search, Plus} from "lucide-react";
import "./style.css"
import {Button} from "@/app/components/ui/button";
import Modal from "@/app/components/modal";
@@ -18,8 +18,33 @@ interface IngredientInput {
unit: string;
}
+interface Suggestion {
+ name: string;
+ unit: string | null;
+}
+
+// Reine Helper-Funktion auf Modulebene – schließt keinen Component-State ein,
+// damit useEffect/useCallback keine ändernden Closure-Variablen aufnehmen müssen.
+async function loadTopIngredients(): Promise {
+ try {
+ const res = await fetchWithAuth(`/ingredients/top?limit=5&_=${Date.now()}`, {
+ cache: "no-store",
+ headers: { "Cache-Control": "no-cache" },
+ });
+ if (!res.ok) return null;
+ const data = await res.json();
+ if (Array.isArray(data.ingredients)) {
+ localStorage.setItem("ingredientSuggestions", JSON.stringify(data.ingredients));
+ return data.ingredients as Suggestion[];
+ }
+ return null;
+ } catch {
+ return null;
+ }
+}
+
export default function RecipeFinder() {
- const { user, loading, logout } = useAuth();
+ const { user, loading } = useAuth();
const router = useRouter();
const [open, setOpen] = useState(false);
@@ -47,12 +72,43 @@ export default function RecipeFinder() {
const [modalOpen, setModalOpen] = useState(false);
+ // Vorschläge: localStorage als sofortiger Initialwert (instant beim Öffnen),
+ // im Hintergrund per useEffect aktualisiert.
+ const [suggestions, setSuggestions] = useState(() => {
+ try {
+ const saved = localStorage.getItem("ingredientSuggestions");
+ return saved ? JSON.parse(saved) : [];
+ } catch {
+ return [];
+ }
+ });
+
useEffect(() => {
if (!loading && !user) {
router.replace("/");
}
}, [loading, user, router]);
+ // Initial nach dem Login laden (damit sie beim ersten Popup-Öffnen sofort da sind)
+ useEffect(() => {
+ if (!user) return;
+ let cancelled = false;
+ loadTopIngredients().then(result => {
+ if (!cancelled && result) setSuggestions(result);
+ });
+ return () => { cancelled = true; };
+ }, [user]);
+
+ // Bei jedem Öffnen des Popups erneut laden – aktuelle Daten nach jeder Suche
+ useEffect(() => {
+ if (!modalOpen || !user) return;
+ let cancelled = false;
+ loadTopIngredients().then(result => {
+ if (!cancelled && result) setSuggestions(result);
+ });
+ return () => { cancelled = true; };
+ }, [modalOpen, user]);
+
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
@@ -115,6 +171,12 @@ export default function RecipeFinder() {
});
const data = await res.json();
setResults(data.rezepte ?? []);
+ // Aktualisierte Top 5 direkt aus der Search-Response übernehmen –
+ // beim nächsten Popup-Öffnen sind die Vorschläge sofort aktuell, ohne extra Roundtrip
+ if (Array.isArray(data.topIngredients)) {
+ setSuggestions(data.topIngredients);
+ localStorage.setItem("ingredientSuggestions", JSON.stringify(data.topIngredients));
+ }
} catch {
setSearchError("Suche fehlgeschlagen.");
} finally {
@@ -150,7 +212,15 @@ export default function RecipeFinder() {
Zutaten
-
setModalOpen(true)} className="finder-sidebar__add-btn">
+ {
+ loadTopIngredients().then(result => {
+ if (result) setSuggestions(result);
+ });
+ }}
+ onClick={() => setModalOpen(true)}
+ className="finder-sidebar__add-btn"
+ >
Zutat hinzufügen
@@ -240,6 +310,7 @@ export default function RecipeFinder() {
onAdd={handleAdd}
servings={servings}
onServingsChange={setServings}
+ suggestions={suggestions}
/>
diff --git a/project/frontend/app/recipeFinder/popup.tsx b/project/frontend/app/recipeFinder/popup.tsx
index ec2f81b..444545b 100644
--- a/project/frontend/app/recipeFinder/popup.tsx
+++ b/project/frontend/app/recipeFinder/popup.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useRef, useState } from "react";
import "./style.css";
const EINHEITEN = ["Stück", "g", "kg", "ml", "l", "EL", "TL", "Prise"];
@@ -9,18 +9,25 @@ export interface IngredientInput {
unit: string;
}
+export interface Suggestion {
+ name: string;
+ unit: string | null;
+}
+
interface Props {
ingredients: IngredientInput[];
onAdd: (ingredient: IngredientInput) => void;
servings: number;
onServingsChange: (s: number) => void;
+ suggestions?: Suggestion[];
}
-export default function AddIngredientsPopup({ ingredients, onAdd}: Props) {
+export default function AddIngredientsPopup({ ingredients, onAdd, suggestions = [] }: Props) {
const [ingredientName, setIngredientName] = useState("");
const [ingredientAmount, setIngredientAmount] = useState("");
const [ingredientUnit, setIngredientUnit] = useState("Stück");
const [inputError, setInputError] = useState("");
+ const amountInputRef = useRef
(null);
const handleAddIngredient = () => {
const trimmedName = ingredientName.trim();
@@ -40,6 +47,21 @@ export default function AddIngredientsPopup({ ingredients, onAdd}: Props) {
setInputError("");
};
+ const handleSuggestionClick = (s: Suggestion) => {
+ setIngredientName(s.name);
+ if (s.unit && EINHEITEN.includes(s.unit)) {
+ setIngredientUnit(s.unit);
+ }
+ setInputError("");
+ // Fokus aufs Mengen-Feld – User muss nur noch die Menge tippen
+ setTimeout(() => amountInputRef.current?.focus(), 0);
+ };
+
+ // Vorschläge bleiben sichtbar – bereits hinzugefügte werden disabled markiert,
+ // damit das Popup seine Größe behält.
+ const isAlreadyAdded = (s: Suggestion) =>
+ ingredients.some(z => z.name.toLowerCase() === s.name.toLowerCase());
+
return (
Zutaten Hinzufügen
@@ -54,6 +76,7 @@ export default function AddIngredientsPopup({ ingredients, onAdd}: Props) {
className="popup__input"
/>
setIngredientAmount(e.target.value)}
@@ -71,6 +94,29 @@ export default function AddIngredientsPopup({ ingredients, onAdd}: Props) {
+ {suggestions.length > 0 && (
+
+
Häufig verwendet
+
+ {suggestions.map(s => {
+ const added = isAlreadyAdded(s);
+ return (
+ !added && handleSuggestionClick(s)}
+ className={`popup__suggestion-badge${added ? " popup__suggestion-badge--added" : ""}`}
+ title={added ? "Bereits hinzugefügt" : (s.unit ? `${s.name} (${s.unit})` : s.name)}
+ >
+ {s.name}
+
+ );
+ })}
+
+
+ )}
+
{inputError && {inputError}
}
@@ -78,4 +124,4 @@ export default function AddIngredientsPopup({ ingredients, onAdd}: Props) {
);
-}
\ No newline at end of file
+}
diff --git a/project/frontend/app/recipeFinder/style.css b/project/frontend/app/recipeFinder/style.css
index f32da98..63041c9 100644
--- a/project/frontend/app/recipeFinder/style.css
+++ b/project/frontend/app/recipeFinder/style.css
@@ -147,6 +147,62 @@
margin: 0;
}
+/* ── Zutaten-Vorschläge (Top 5 Badges) ─────────────────────── */
+.popup__suggestions {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.popup__suggestions-label {
+ font-size: 12px;
+ font-weight: 600;
+ color: #6b7280;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin: 0;
+ font-family: system-ui;
+}
+
+.popup__suggestions-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.popup__suggestion-badge {
+ padding: 5px 12px;
+ background: #f3f3f5;
+ border: 1px solid #e5e7eb;
+ border-radius: 999px;
+ font-size: 13px;
+ font-family: system-ui;
+ color: #111;
+ cursor: pointer;
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
+}
+
+.popup__suggestion-badge:hover {
+ background: #030213;
+ border-color: #030213;
+ color: #fff;
+}
+
+.popup__suggestion-badge:focus-visible {
+ outline: 2px solid #030213;
+ outline-offset: 2px;
+}
+
+.popup__suggestion-badge--added,
+.popup__suggestion-badge--added:hover {
+ background: #f3f3f5;
+ border-color: #e5e7eb;
+ color: #9ca3af;
+ cursor: default;
+ text-decoration: line-through;
+ opacity: 0.6;
+}
+
.popup__btn {
display: block;
margin: 0 auto;
diff --git a/project/frontend/lib/auth.tsx b/project/frontend/lib/auth.tsx
index 2d6c1ca..64c79d9 100644
--- a/project/frontend/lib/auth.tsx
+++ b/project/frontend/lib/auth.tsx
@@ -49,6 +49,7 @@ function clearTokens() {
sessionStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
localStorage.removeItem("ingredients");
+ localStorage.removeItem("ingredientSuggestions");
}
// ── API-Aufrufe ───────────────────────────────────────────────
@@ -107,7 +108,7 @@ async function fetchWithAuth(url: string, options: RequestInit = {}): Promise
Date: Mon, 11 May 2026 13:33:16 +0200
Subject: [PATCH 28/85] Black used
---
project/backend/Auth.py | 10 ++++--
project/backend/Database.py | 57 +++++++++++++++++++++++-----------
project/backend/Ingredient.py | 9 +++---
project/backend/Models.py | 5 ++-
project/backend/Recipe.py | 7 ++---
project/backend/RecipeSucuk.py | 45 ++++++++++++++++-----------
project/backend/Routes.py | 36 ++++++++++++---------
7 files changed, 108 insertions(+), 61 deletions(-)
diff --git a/project/backend/Auth.py b/project/backend/Auth.py
index 696fce6..2447716 100644
--- a/project/backend/Auth.py
+++ b/project/backend/Auth.py
@@ -136,8 +136,10 @@ async def getCurrentUser(token: Annotated[str, Depends(oauth2_scheme)]) -> User:
return User(email=konto["email"], name=konto["name"])
+
# --- Reset Password --- #
+
def hashResetToken(token: str) -> str:
"""SHA-256 Hash – für Reset-Tokens reicht das, sie sind ohnehin hochentropisch."""
return hashlib.sha256(token.encode()).hexdigest()
@@ -146,14 +148,18 @@ def hashResetToken(token: str) -> str:
def createPasswordResetToken(kontoId: int) -> str:
"""Generiert Klartext-Token (geht per Mail) und speichert nur den Hash in der DB."""
from Database import savePasswordResetToken
+
token = secrets.token_urlsafe(48)
- expiresAt = datetime.now(timezone.utc) + timedelta(minutes=PASSWORD_RESET_EXPIRE_MINUTES)
+ expiresAt = datetime.now(timezone.utc) + timedelta(
+ minutes=PASSWORD_RESET_EXPIRE_MINUTES
+ )
savePasswordResetToken(kontoId, hashResetToken(token), expiresAt.isoformat())
return token
def validatePasswordResetToken(token: str) -> dict | None:
from Database import getPasswordResetToken
+
entry = getPasswordResetToken(hashResetToken(token))
if entry is None or entry["usedAt"] is not None:
return None
@@ -162,4 +168,4 @@ def validatePasswordResetToken(token: str) -> dict | None:
expiresAt = expiresAt.replace(tzinfo=timezone.utc)
if expiresAt < datetime.now(timezone.utc):
return None
- return entry
\ No newline at end of file
+ return entry
diff --git a/project/backend/Database.py b/project/backend/Database.py
index 9ad9c44..580ad41 100644
--- a/project/backend/Database.py
+++ b/project/backend/Database.py
@@ -3,7 +3,6 @@
from pathlib import Path
import os
-
# Verwende die Datenbank aus dem data-Ordner
DB_PATH = Path(__file__).parent.parent / "data" / "LazyCookDB.sqlite3"
@@ -236,9 +235,10 @@ def cleanupExpiredTokens() -> None:
with getDB() as con:
cur = con.cursor()
cur.execute("DELETE FROM RefreshToken WHERE expiresAt < datetime('now')")
-
+
+
def addRecipe(name: str, description: str, vid: int) -> int:
- with getDB() as con:
+ with getDB() as con:
cur = con.cursor()
cur.execute(
"INSERT INTO Recipe (name, description) VALUES (?, ?)",
@@ -246,6 +246,7 @@ def addRecipe(name: str, description: str, vid: int) -> int:
)
return cur.lastrowid
+
def addIngredientToRecipe(zid: int, rid: int, amount: float) -> int:
with getDB() as con:
cur = con.cursor()
@@ -253,7 +254,8 @@ def addIngredientToRecipe(zid: int, rid: int, amount: float) -> int:
"INSERT INTO Exists_from (zid, rid, amount) VALUES (?, ?, ?)",
(zid, rid, amount),
)
-
+
+
def addIngredient(name: str, amountType: str) -> int:
with getDB() as con:
cur = con.cursor()
@@ -262,31 +264,40 @@ def addIngredient(name: str, amountType: str) -> int:
(name, amountType),
)
return cur.lastrowid
-
+
+
def getIngridientByName(name: str):
- with getDB() as con:
+ with getDB() as con:
cur = con.cursor()
- cur.execute("""
+ cur.execute(
+ """
SELECT id, amountType
FROM Recipe
WHERE id = ?
- """, (name))
+ """,
+ (name),
+ )
row = cur.fetchone()
return dict(row) if row else None
+
def getRecipe(recipeID: int) -> dict | None:
"""Gibt eine Liste aller Rezepte zurück."""
with getDB() as con:
cur = con.cursor()
- cur.execute("""
+ cur.execute(
+ """
SELECT name, description
FROM Recipe
WHERE id = ?
- """, (recipeID))
+ """,
+ (recipeID),
+ )
row = cur.fetchone()
return dict(row) if row else None
-def getAllRecipes()-> list[dict]:
+
+def getAllRecipes() -> list[dict]:
with getDB() as con:
cur = con.cursor()
cur.execute("""
@@ -295,10 +306,13 @@ def getAllRecipes()-> list[dict]:
""")
rows = cur.fetchall()
return [dict(row) for row in rows]
+
+
def getAllIngredientsForRecipe(rid: int):
with getDB() as con:
cur = con.cursor()
- cur.execute("""
+ cur.execute(
+ """
SELECT
Ingredient.name,
Exists_from.amount,
@@ -307,20 +321,26 @@ def getAllIngredientsForRecipe(rid: int):
JOIN Exists_from ON Ingredient.id = Exists_from.zid
JOIN Recipe ON Exists_from.rid = Recipe.id
WHERE Exists_from.rid = ?
- """, (rid,)) # Note the comma here!
+ """,
+ (rid,),
+ ) # Note the comma here!
rows = cur.fetchall()
return [dict(row) for row in rows]
-def getAllocatedRecipes(name: str)-> list[dict]:
+
+def getAllocatedRecipes(name: str) -> list[dict]:
with getDB() as con:
cur = con.cursor()
- cur.execute("""
+ cur.execute(
+ """
SELECT rid
FROM Exists_from
Inner Join Ingredient on rid=id,
Where Exists_from.id = ?
- """, (name,))
+ """,
+ (name,),
+ )
rows = cur.fetchall()
return [dict(row) for row in rows]
@@ -420,9 +440,9 @@ def getAllocatedRecipes(name: str)-> list[dict]:
"""
-
# ── Password-Reset-Token-Operationen ───────────────────────────
+
def savePasswordResetToken(kontoID: int, tokenHash: str, expiresAt: str) -> None:
"""Invalidiert alte Tokens des Kontos und speichert einen neuen."""
with getDB() as con:
@@ -474,6 +494,7 @@ def updateKontoPassword(konto_id: int, hashed_password: str) -> None:
# ── Ingredient-Usage-Operationen ───────────────────────────────
+
def incrementIngredientUsage(AccountID: int, name: str, unit: str | None) -> None:
"""Erhöht den Usage-Counter für eine Zutat um 1, aktualisiert lastUnit und lastUsedAt.
Legt einen neuen Eintrag an, falls die Zutat für diesen Account noch nicht existiert.
@@ -534,4 +555,4 @@ def getAccountById(konto_id: int) -> dict | None:
row = cur.fetchone()
return dict(row) if row else None
finally:
- con.close()
\ No newline at end of file
+ con.close()
diff --git a/project/backend/Ingredient.py b/project/backend/Ingredient.py
index ee9228d..154caba 100644
--- a/project/backend/Ingredient.py
+++ b/project/backend/Ingredient.py
@@ -1,11 +1,12 @@
from typing import List, Dict, Any
from Database import addIngredient
+
class Ingredient:
def __init__(self, name: str, amount: float):
self.__name = name
self.__amount = amount
-
+
def getName(self) -> str:
return self.__name
@@ -17,13 +18,13 @@ def getAmount(self) -> float:
def setAmount(self, amount: float):
self.__amount = amount
-
+
def setAmountType(self, amountType: str):
self.__amountType = amountType
-
+
def getAmountType(self):
return self.__amountType
-
+
def saveInDB(self):
if not self.__amountType:
return False
diff --git a/project/backend/Models.py b/project/backend/Models.py
index 12d4943..462b9db 100644
--- a/project/backend/Models.py
+++ b/project/backend/Models.py
@@ -26,13 +26,16 @@ class RefreshRequest(BaseModel):
class LogoutRequest(BaseModel):
refresh_token: str
+
class ForgotPasswordRequest(BaseModel):
email: str
+
class ResetPasswordRequest(BaseModel):
token: str
new_password: str
+
class UpdateUser(BaseModel):
email: str | None = None
currentPassword: str | None = None
@@ -47,4 +50,4 @@ class IngredientSearch(BaseModel):
class RecipeSearchRequest(BaseModel):
zutaten: list[IngredientSearch]
- servings: int
\ No newline at end of file
+ servings: int
diff --git a/project/backend/Recipe.py b/project/backend/Recipe.py
index 51925e2..b9ce137 100644
--- a/project/backend/Recipe.py
+++ b/project/backend/Recipe.py
@@ -1,6 +1,7 @@
-
from Ingredient import Ingredient
from Database import addIngredientToRecipe, addRecipe, getIngridientByName
+
+
class Recipe:
def __init__(self, name: str, ingredients: list[Ingredient], description: str):
self.__name = name
@@ -11,7 +12,6 @@ def __init__(self, name: str, ingredients: list[Ingredient], description: str):
self.__rating = 0.0
self.__countPersons = 1
self.__matching = 0
-
def saveInDB(self) -> bool:
rid = addRecipe(self.__name, self.__description, None)
@@ -51,7 +51,7 @@ def getMatching(self) -> int:
return self.__matching
def incrementMatching(self):
- self.__matching+=1
+ self.__matching += 1
def getDuration(self) -> str:
return self.__duration
@@ -64,4 +64,3 @@ def getingredients(self) -> list[Ingredient]:
def setIngredient(self, ingredients: list[Ingredient]):
self.__ingredients = ingredients
-
diff --git a/project/backend/RecipeSucuk.py b/project/backend/RecipeSucuk.py
index 64aafc3..56834e3 100644
--- a/project/backend/RecipeSucuk.py
+++ b/project/backend/RecipeSucuk.py
@@ -1,16 +1,17 @@
import Database
from Ingredient import Ingredient
from Recipe import Recipe
-from Database import getAllRecipes, getAllIngredientsForRecipe
+from Database import getAllRecipes, getAllIngredientsForRecipe
-def findRecipes(ingriedents: list[Ingredient])-> list[Recipe]:
+
+def findRecipes(ingriedents: list[Ingredient]) -> list[Recipe]:
recipes = __initRecipes()
-
+
for Ingredient in ingriedents:
recipes = __filterRecipes(recipes, Ingredient)
recipes.sort(key=lambda x: x.getRating(), reverse=True)
-
+
for recipe in recipes[::-1]:
if recipe.getRating() < 0.3:
recipes.remove(recipe)
@@ -18,37 +19,45 @@ def findRecipes(ingriedents: list[Ingredient])-> list[Recipe]:
break
return recipes
-def __filterRecipes(recipes: list[Recipe], Ingredient: Ingredient)-> list[Recipe]:
+
+def __filterRecipes(recipes: list[Recipe], Ingredient: Ingredient) -> list[Recipe]:
for recipe in recipes:
for recipeIngredient in recipe.getIngredients():
if recipeIngredient.getName() == Ingredient.getName():
recipe.incrementMatching()
- recipe.setRating(recipe.getMatching()/ len(recipe.getIngredients()))
+ recipe.setRating(recipe.getMatching() / len(recipe.getIngredients()))
return recipes
-def __initRecipes()-> list[Recipe]:
+
+def __initRecipes() -> list[Recipe]:
recipesRaw = getAllRecipes()
recipes = []
for recipeRaw in recipesRaw:
- recipe = Recipe(recipeRaw["name"], __formatIngredients(recipeRaw["id"]), recipeRaw["description"])
+ recipe = Recipe(
+ recipeRaw["name"],
+ __formatIngredients(recipeRaw["id"]),
+ recipeRaw["description"],
+ )
recipes.append(recipe)
return recipes
-def __formatIngredients(id: int)-> list[Ingredient]:
+
+def __formatIngredients(id: int) -> list[Ingredient]:
IngredientsRaw = getAllIngredientsForRecipe(id)
Ingredients = []
for IngredientRaw in IngredientsRaw:
- Ingredients.append(Ingredient(IngredientRaw["name"],IngredientRaw["amount"]))
+ Ingredients.append(Ingredient(IngredientRaw["name"], IngredientRaw["amount"]))
return Ingredients
+
ini = []
-ini.append(Ingredient("Linguine", 10 ))
-ini.append(Ingredient("Fresh Basil", 10 ))
-ini.append(Ingredient("Pine Nuts", 10 ))
-ini.append(Ingredient("Parmesan", 10 ))
-ini.append(Ingredient("Ground Beef", 10 ))
-ini.append(Ingredient("Cumin", 10 ))
-ini.append(Ingredient("Cucumber", 10 ))
+ini.append(Ingredient("Linguine", 10))
+ini.append(Ingredient("Fresh Basil", 10))
+ini.append(Ingredient("Pine Nuts", 10))
+ini.append(Ingredient("Parmesan", 10))
+ini.append(Ingredient("Ground Beef", 10))
+ini.append(Ingredient("Cumin", 10))
+ini.append(Ingredient("Cucumber", 10))
arr = findRecipes(ini)
for i in arr:
- print(i.getName()+ " "+ str(i.getRating()))
\ No newline at end of file
+ print(i.getName() + " " + str(i.getRating()))
diff --git a/project/backend/Routes.py b/project/backend/Routes.py
index b02bd65..868108d 100644
--- a/project/backend/Routes.py
+++ b/project/backend/Routes.py
@@ -30,7 +30,17 @@
getTopIngredients,
)
-from Models import User, Token, UserCreate, RefreshRequest, LogoutRequest, ForgotPasswordRequest, ResetPasswordRequest, UpdateUser, RecipeSearchRequest
+from Models import (
+ User,
+ Token,
+ UserCreate,
+ RefreshRequest,
+ LogoutRequest,
+ ForgotPasswordRequest,
+ ResetPasswordRequest,
+ UpdateUser,
+ RecipeSearchRequest,
+)
# Frontend-URL aus Env, mit Dev-Fallback (Frontend läuft per compose.yaml auf Port 8000)
FRONTEND_URL = os.environ.get("FRONTEND_URL", "http://localhost:8000")
@@ -116,8 +126,6 @@ async def deleteCurrentUser(currentUser: Annotated[User, Depends(getCurrentUser)
# ── Account aktualisieren ────────────────────────────────────────
-
-
@router.patch("/users/me")
async def updateCurrentUser(
data: UpdateUser, currentUser: Annotated[User, Depends(getCurrentUser)]
@@ -152,8 +160,10 @@ async def updateCurrentUser(
return {"success": True}
+
# ------------ Passwort vergessen ----------------- #
+
@router.post("/auth/forgot-password")
async def forgotPassword(body: ForgotPasswordRequest):
"""Schritt 1: User gibt E-Mail ein, bekommt Reset-Link per Mail."""
@@ -204,10 +214,11 @@ async def resetPassword(body: ResetPasswordRequest):
# ── Recipe-Suche ───────────────────────────────────────────────
+
@router.post("/recipes/search")
async def searchRecipes(
- body: RecipeSearchRequest,
- currentUser: Annotated[User, Depends(getCurrentUser)],
+ body: RecipeSearchRequest,
+ currentUser: Annotated[User, Depends(getCurrentUser)],
):
"""Sucht Rezepte basierend auf den übergebenen Zutaten.
@@ -227,8 +238,7 @@ async def searchRecipes(
# Aktualisierte Top 5 direkt mit zurückgeben → Frontend muss keinen Extra-Request machen
topRows = getTopIngredients(Account["id"], limit=5)
topIngredients = [
- {"name": r["displayName"], "unit": r["lastUnit"]}
- for r in topRows
+ {"name": r["displayName"], "unit": r["lastUnit"]} for r in topRows
]
# TODO: eigentliche Rezept-Suche implementieren
@@ -237,11 +247,12 @@ async def searchRecipes(
# ── Ingredient-Vorschläge ──────────────────────────────────────
+
@router.get("/ingredients/top")
async def getTopIngredientsForUser(
- response: Response,
- currentUser: Annotated[User, Depends(getCurrentUser)],
- limit: int = 5,
+ response: Response,
+ currentUser: Annotated[User, Depends(getCurrentUser)],
+ limit: int = 5,
):
"""Liefert die meistgenutzten Zutaten des aktuellen Users für die Vorschlags-Badges."""
Account = getAccountByEmail(currentUser.email)
@@ -254,8 +265,5 @@ async def getTopIngredientsForUser(
rows = getTopIngredients(Account["id"], limit=limit)
return {
- "ingredients": [
- {"name": r["displayName"], "unit": r["lastUnit"]}
- for r in rows
- ]
+ "ingredients": [{"name": r["displayName"], "unit": r["lastUnit"]} for r in rows]
}
From 3c02e9f97ac379ddee9d341c56c81f06030ec537 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Mon, 11 May 2026 13:42:41 +0200
Subject: [PATCH 29/85] Info: Standort von Impressum und Datenschutz
---
project/frontend/app/homepage/homepage.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/project/frontend/app/homepage/homepage.tsx b/project/frontend/app/homepage/homepage.tsx
index 6ad3795..c00423a 100644
--- a/project/frontend/app/homepage/homepage.tsx
+++ b/project/frontend/app/homepage/homepage.tsx
@@ -27,6 +27,7 @@ export default function Homepage() {
From a7ceb96941a22a4b774d39548e942594424008c5 Mon Sep 17 00:00:00 2001
From: F <150372421+GalacticCodeGambit@users.noreply.github.com>
Date: Tue, 12 May 2026 16:59:55 +0200
Subject: [PATCH 30/85] Added Review-Protokoll-01
---
docs/Review-Protokoll-01.md | 34 ++++++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
create mode 100644 docs/Review-Protokoll-01.md
diff --git a/docs/Review-Protokoll-01.md b/docs/Review-Protokoll-01.md
new file mode 100644
index 0000000..fe41c5c
--- /dev/null
+++ b/docs/Review-Protokoll-01.md
@@ -0,0 +1,34 @@
+# Review-Protokoll-01
+### **Datum** (Startzeit und Endzeit):
+12.05.2026 16.:00 bis 16:20
+
+### **Teilnehmende**:
+- Moderator: Samuel
+- Zeitwächter: Niclas
+- Notizen: Frederik
+- Teilnehmer: Eden, Alexander
+
+### **Ziel**/**Schwerpunkt** des Reviews:
+Schwerpunkt: Team übergreifenden Verständnis des Algorithmus Rezept Suche verbessern.
+Warum: Umfangreiches Verständnis schaffen, da es sich um ein Zentrales Feature handelt und um die Umsetzung der Rezept Anzeige bewerten zu können.
+
+Planen/Umsetzung von Features die auf den Algorithmus darauf Bauen.
+
+### **Komponenten für den Review**:
+Datei: `RecipeSucuk.py`
+
+### **Kriterien für den Review**:
+- Codequalität
+- Performance soll auf 100 Rezepten in unter 1s laufen
+- Testabdeckung
+- Wartbarkeit
+
+### **Review-Methodik**:
+Walkthroughs und Code Review von `RecipeSucuk.py`
+
+### **Ergebnisse**:
+- Umsetzung der Anzeigen der Rezepte
+ Top 100 Matching Rezepte werden an Frontend geschickt von Algorithmus.
+ Danach ist der Mehr-Button nicht mehr da.
+- Müssen Test schreiben für die Datei `RecipeSucuk.py`.
+- Akronyme für `RecipeSucuk.py` wurde bestimmt (SUCUK - Search for Uncomplicated Cooking and User-friendly Kitchen recipes)
From 42e06bb9fc07a69facf02f12b6bbd7089264068e Mon Sep 17 00:00:00 2001
From: F <150372421+GalacticCodeGambit@users.noreply.github.com>
Date: Tue, 12 May 2026 17:01:20 +0200
Subject: [PATCH 31/85] Clear participant roles in review protocol
Removed participant names from the review protocol.
---
docs/Review-Protokoll-01.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/Review-Protokoll-01.md b/docs/Review-Protokoll-01.md
index fe41c5c..741f878 100644
--- a/docs/Review-Protokoll-01.md
+++ b/docs/Review-Protokoll-01.md
@@ -3,10 +3,10 @@
12.05.2026 16.:00 bis 16:20
### **Teilnehmende**:
-- Moderator: Samuel
-- Zeitwächter: Niclas
-- Notizen: Frederik
-- Teilnehmer: Eden, Alexander
+- Moderator:
+- Zeitwächter:
+- Notizen:
+- Teilnehmer:
### **Ziel**/**Schwerpunkt** des Reviews:
Schwerpunkt: Team übergreifenden Verständnis des Algorithmus Rezept Suche verbessern.
From e27f003e8ab661a3d8dc5f146e09062661e1b0c5 Mon Sep 17 00:00:00 2001
From: F <150372421+GalacticCodeGambit@users.noreply.github.com>
Date: Tue, 12 May 2026 17:03:10 +0200
Subject: [PATCH 32/85] Update participants and review focus in documentation
---
docs/Review-Protokoll-01.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/Review-Protokoll-01.md b/docs/Review-Protokoll-01.md
index 741f878..5ce09da 100644
--- a/docs/Review-Protokoll-01.md
+++ b/docs/Review-Protokoll-01.md
@@ -3,10 +3,10 @@
12.05.2026 16.:00 bis 16:20
### **Teilnehmende**:
-- Moderator:
-- Zeitwächter:
-- Notizen:
-- Teilnehmer:
+- Moderator: PrussianBaron
+- Zeitwächter: Nicoolaus
+- Notizen: GalacticCodeGambit
+- Teilnehmer: EdenBernhard, Hellocrafting
### **Ziel**/**Schwerpunkt** des Reviews:
Schwerpunkt: Team übergreifenden Verständnis des Algorithmus Rezept Suche verbessern.
From 3792403483b39b8988c7434554f18f959f203659 Mon Sep 17 00:00:00 2001
From: Samuel Goebel
Date: Mon, 18 May 2026 12:40:40 +0200
Subject: [PATCH 33/85] Made the required RecipeSUCUK changes
---
project/backend/RecipeSucuk.py | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/project/backend/RecipeSucuk.py b/project/backend/RecipeSucuk.py
index 56834e3..b41c3e6 100644
--- a/project/backend/RecipeSucuk.py
+++ b/project/backend/RecipeSucuk.py
@@ -1,3 +1,4 @@
+# SUCUK = Search for Uncomplicated Cooking and User-friendly Kitchen recipes
import Database
from Ingredient import Ingredient
from Recipe import Recipe
@@ -12,11 +13,9 @@ def findRecipes(ingriedents: list[Ingredient]) -> list[Recipe]:
recipes.sort(key=lambda x: x.getRating(), reverse=True)
- for recipe in recipes[::-1]:
- if recipe.getRating() < 0.3:
- recipes.remove(recipe)
- else:
- break
+ if len(recipes) > 98:
+ return recipes[:98]
+
return recipes
From 5db1e8d89c81bdc6e4287083c83a76e07450adfd Mon Sep 17 00:00:00 2001
From: PrussianBaron
Date: Mon, 18 May 2026 12:45:29 +0200
Subject: [PATCH 34/85] Rename RecipeSucuk.py to RecipeSUCUK.py
---
project/backend/{RecipeSucuk.py => RecipeSUCUK.py} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename project/backend/{RecipeSucuk.py => RecipeSUCUK.py} (100%)
diff --git a/project/backend/RecipeSucuk.py b/project/backend/RecipeSUCUK.py
similarity index 100%
rename from project/backend/RecipeSucuk.py
rename to project/backend/RecipeSUCUK.py
From 74f6e207716e6cc3a61051d0c5ce41ef33e4d6f5 Mon Sep 17 00:00:00 2001
From: Samuel Goebel
Date: Mon, 18 May 2026 13:14:52 +0200
Subject: [PATCH 35/85] Added Search Algorithm for Recipe Name search
---
project/backend/SearchRecipeNames.py | 10 ++++++++++
1 file changed, 10 insertions(+)
create mode 100644 project/backend/SearchRecipeNames.py
diff --git a/project/backend/SearchRecipeNames.py b/project/backend/SearchRecipeNames.py
new file mode 100644
index 0000000..b448372
--- /dev/null
+++ b/project/backend/SearchRecipeNames.py
@@ -0,0 +1,10 @@
+from Database import getAllRecipes
+
+def getMatchingRecipeNames(searchTerm: str) -> list[str]:
+ """Gibt eine Liste von Rezeptnamen zurück, die den Suchbegriff enthalten."""
+ searchTerm = searchTerm.lower()
+ matchingRecipes = []
+ for recipe in getAllRecipes():
+ if searchTerm in recipe["name"].lower():
+ matchingRecipes.append(recipe["name"])
+ return matchingRecipes
\ No newline at end of file
From 4f256137e89bda25c689f07ec5a4c629415a107a Mon Sep 17 00:00:00 2001
From: Samuel Goebel
Date: Mon, 18 May 2026 14:45:42 +0200
Subject: [PATCH 36/85] SearchRecipeNames now returns a list of recipe Objects
---
project/backend/SearchRecipeNames.py | 19 ++++++++++++++-----
1 file changed, 14 insertions(+), 5 deletions(-)
diff --git a/project/backend/SearchRecipeNames.py b/project/backend/SearchRecipeNames.py
index b448372..1b51649 100644
--- a/project/backend/SearchRecipeNames.py
+++ b/project/backend/SearchRecipeNames.py
@@ -1,10 +1,19 @@
-from Database import getAllRecipes
+from Database import getAllRecipes, getAllIngredientsForRecipe
+from Ingredient import Ingredient
+from Recipe import Recipe
-def getMatchingRecipeNames(searchTerm: str) -> list[str]:
- """Gibt eine Liste von Rezeptnamen zurück, die den Suchbegriff enthalten."""
+def getMatchingRecipeNames(searchTerm: str) -> list[Recipe]:
+ """Gibt eine Liste von Rezepten zurück, die den Suchbegriff enthalten."""
searchTerm = searchTerm.lower()
matchingRecipes = []
for recipe in getAllRecipes():
if searchTerm in recipe["name"].lower():
- matchingRecipes.append(recipe["name"])
- return matchingRecipes
\ No newline at end of file
+ matchingRecipes.append(Recipe(recipe["name"], __formatIngredients(recipe["id"]), recipe["description"]))
+ return matchingRecipes
+
+def __formatIngredients(id: int) -> list[Ingredient]:
+ IngredientsRaw = getAllIngredientsForRecipe(id)
+ Ingredients = []
+ for IngredientRaw in IngredientsRaw:
+ Ingredients.append(Ingredient(IngredientRaw["name"], IngredientRaw["amount"]))
+ return Ingredients
\ No newline at end of file
From 8961073fe002ad1fdb8a3b49df9d0ff755226ec2 Mon Sep 17 00:00:00 2001
From: GalacticCodeGambit
<150372421+GalacticCodeGambit@users.noreply.github.com>
Date: Mon, 18 May 2026 15:50:32 +0200
Subject: [PATCH 37/85] Add SonarCloud configuration and update requirements
for coverage reporting
---
.github/workflows/sonarqube.yml | 62 ++++++++++++++++++++++++++++++++
project/backend/requirements.txt | 3 +-
sonar-project.properties | 13 +++++++
3 files changed, 77 insertions(+), 1 deletion(-)
create mode 100644 .github/workflows/sonarqube.yml
create mode 100644 sonar-project.properties
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
new file mode 100644
index 0000000..cd05426
--- /dev/null
+++ b/.github/workflows/sonarqube.yml
@@ -0,0 +1,62 @@
+name: "SonarCloud Analysis"
+
+on:
+ pull_request:
+ branches: [main, 'blatt*', 'feat/*']
+ push:
+ branches: [main, 'blatt*', 'feat/*']
+
+permissions:
+ contents: read
+ pull-requests: read
+
+jobs:
+ analyze:
+ name: SonarCloud scan
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python 3.11
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Install backend dependencies
+ working-directory: project/backend
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+
+ - name: Run backend tests with coverage
+ working-directory: project/backend
+ run: |
+ python -m pytest --cov=. --cov-report=xml:coverage.xml --cov-report=term-missing
+
+ - name: Set up Java 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '17'
+
+ - name: Cache SonarQube scanner
+ uses: actions/cache@v4
+ with:
+ path: ~/.sonar/cache
+ key: ${{ runner.os }}-sonar
+ restore-keys: ${{ runner.os }}-sonar
+
+ - name: Run SonarCloud analysis
+ uses: SonarSource/sonarqube-scan-action@v6
+ with:
+ args: >
+ -Dsonar.organization=${{ secrets.SONAR_ORGANIZATION }}
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_HOST_URL: https://sonarcloud.io
+
+
diff --git a/project/backend/requirements.txt b/project/backend/requirements.txt
index 60137ce..3b3b797 100644
--- a/project/backend/requirements.txt
+++ b/project/backend/requirements.txt
@@ -5,4 +5,5 @@ gunicorn==23.0.0
bcrypt
python-jose[cryptography]
python-multipart
-pytest==9.0.3
\ No newline at end of file
+pytest==9.0.3
+pytest-cov==6.0.0
\ No newline at end of file
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000..b4aa963
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,13 @@
+sonar.projectKey=LazyCook
+sonar.projectName=LazyCook
+sonar.sourceEncoding=UTF-8
+
+sonar.sources=project/backend,project/frontend
+sonar.tests=project/backend/tests
+sonar.test.inclusions=project/backend/tests/**/*.py
+sonar.python.coverage.reportPaths=project/backend/coverage.xml
+sonar.coverage.exclusions=project/frontend/**,project/backend/tests/**
+
+sonar.exclusions=**/__pycache__/**,project/data/**,project/frontend/.next/**,project/frontend/node_modules/**,project/backend/coverage.xml
+sonar.python.version=3.11
+
From fa6277fb04a41ac7152338c96a583d660cb51656 Mon Sep 17 00:00:00 2001
From: GalacticCodeGambit
<150372421+GalacticCodeGambit@users.noreply.github.com>
Date: Mon, 18 May 2026 15:53:16 +0200
Subject: [PATCH 38/85] Test Commit
---
sonar-project.properties | 1 -
1 file changed, 1 deletion(-)
diff --git a/sonar-project.properties b/sonar-project.properties
index b4aa963..1b9f265 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -10,4 +10,3 @@ sonar.coverage.exclusions=project/frontend/**,project/backend/tests/**
sonar.exclusions=**/__pycache__/**,project/data/**,project/frontend/.next/**,project/frontend/node_modules/**,project/backend/coverage.xml
sonar.python.version=3.11
-
From a686f9b110257abd494ff1305156b6097b33860b Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Mon, 18 May 2026 23:23:08 +0200
Subject: [PATCH 39/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 232 ++++++++++++++++++++++++++++----
sonar-project.properties | 12 --
2 files changed, 203 insertions(+), 41 deletions(-)
delete mode 100644 sonar-project.properties
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index cd05426..efdb4e8 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -1,18 +1,15 @@
-name: "SonarCloud Analysis"
+name: SonarCloud Analysis
on:
- pull_request:
- branches: [main, 'blatt*', 'feat/*']
push:
- branches: [main, 'blatt*', 'feat/*']
-
-permissions:
- contents: read
- pull-requests: read
+ branches: [main, master, develop]
+ pull_request:
+ branches: [main, master, develop]
+ workflow_dispatch:
jobs:
- analyze:
- name: SonarCloud scan
+ sonarcloud:
+ name: Build, Test & Sonar Scan
runs-on: ubuntu-latest
steps:
@@ -21,42 +18,219 @@ jobs:
with:
fetch-depth: 0
- - name: Set up Python 3.11
+ # =====================================================
+ # BACKEND (Python / FastAPI) – Tests + Coverage
+ # =====================================================
+ - name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: '3.11'
+ python-version: "3.10"
- name: Install backend dependencies
working-directory: project/backend
run: |
python -m pip install --upgrade pip
- pip install -r requirements.txt
+ python -m pip install -r requirements.txt
+ python -m pip install "pytest-cov>=5,<7" "coverage>=7"
- name: Run backend tests with coverage
- working-directory: project/backend
+ working-directory: project
run: |
- python -m pytest --cov=. --cov-report=xml:coverage.xml --cov-report=term-missing
+ python -m pytest \
+ --cov=backend \
+ --cov-report=xml:backend/coverage.xml \
+ --cov-report=term-missing \
+ backend/tests/
+ continue-on-error: true
- - name: Set up Java 17
- uses: actions/setup-java@v4
+ # =====================================================
+ # FRONTEND (Next.js / TypeScript) – Tests + Coverage
+ # =====================================================
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
with:
- distribution: temurin
- java-version: '17'
+ node-version: "20"
+ cache: "npm"
+ cache-dependency-path: project/frontend/package-lock.json
- - name: Cache SonarQube scanner
- uses: actions/cache@v4
+ - name: Install frontend dependencies
+ working-directory: project/frontend
+ run: npm ci
+
+ - name: Run frontend tests with coverage
+ working-directory: project/frontend
+ run: npm run test:coverage --if-present
+ continue-on-error: true
+
+ # =====================================================
+ # SONARCLOUD SCAN
+ # =====================================================
+ - name: SonarQube Scan
+ uses: SonarSource/sonarqube-scan-action@v3
with:
- path: ~/.sonar/cache
- key: ${{ runner.os }}-sonar
- restore-keys: ${{ runner.os }}-sonar
+ projectBaseDir: project
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- - name: Run SonarCloud analysis
- uses: SonarSource/sonarqube-scan-action@v6
+ - name: SonarCloud Quality Gate check
+ id: sonar-qg
+ uses: SonarSource/sonarqube-quality-gate-action@master
+ timeout-minutes: 5
+ continue-on-error: true
with:
- args: >
- -Dsonar.organization=${{ secrets.SONAR_ORGANIZATION }}
+ scanMetadataReportFile: project/.scannerwork/report-task.txt
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- SONAR_HOST_URL: https://sonarcloud.io
+ # =====================================================
+ # METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
+ # =====================================================
+ - name: Render SonarCloud metrics to Job Summary
+ if: always()
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_HOST: https://sonarcloud.io
+ QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
+ run: |
+ set +e
+
+ PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
+ | head -1 | cut -d'=' -f2 | tr -d ' \r\n')
+
+ METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
+
+ API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}"
+ HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
+ -u "${SONAR_TOKEN}:" "${API_URL}")
+ RESP=$(cat /tmp/sonar.json)
+
+ HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
+ SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
+
+ if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
+ {
+ echo "## SonarCloud Analyse"
+ echo ""
+ echo ":warning: Konnte die Metriken nicht abrufen."
+ echo ""
+ echo "- **HTTP Status:** \`${HTTP_CODE}\`"
+ echo "- **Project Key:** \`${PROJECT_KEY}\`"
+ echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
+ } >> "$SUMMARY_FILE"
+ exit 0
+ fi
+
+ get() {
+ echo "${RESP}" | jq -r --arg k "$1" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end'
+ }
+
+ get_sub() {
+ local path_enc sub_resp
+ path_enc=$(printf '%s' "$1" | sed 's|/|%2F|g')
+ sub_resp=$(curl -s -u "${SONAR_TOKEN}:" \
+ "${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}:${path_enc}&metricKeys=$2")
+ echo "${sub_resp}" | jq -r --arg k "$2" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end' 2>/dev/null || echo "-"
+ }
+
+ rating() {
+ case "$(get "$1")" in
+ 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
+ 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
+ esac
+ }
+
+ DEBT_MIN=$(get sqale_index)
+ if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
+ DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
+ DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
+ else
+ DEBT_FMT="-"
+ fi
+
+ if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
+ QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
+ else
+ QG_ICON="FAILED"; QG_EMOJI=":x:"
+ fi
+
+ {
+ echo "## SonarCloud Analyse – LazyCook"
+ echo ""
+ echo "**Quality Gate:** ${QG_EMOJI} \`${QG_ICON}\` "
+ echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
+ echo ""
+ echo "### Coverage"
+ echo ""
+ echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
+ echo "|---|---|---|---|---|"
+ echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
+ echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
+ echo "| Frontend (TS/JS/CSS) | $(get_sub frontend/app coverage)% | $(get_sub frontend/app line_coverage)% | $(get_sub frontend/app branch_coverage)% | $(get_sub frontend/app ncloc) |"
+ echo ""
+
+ echo "### Duplikate & Komplexität"
+ echo ""
+ echo "| Bereich | Metrik | Wert |"
+ echo "|---|---|---|"
+ echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
+ echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
+ echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
+ echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
+ echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
+ echo ""
+
+ echo "### Alle Dateien mit Duplikaten"
+ echo ""
+ } >> "$SUMMARY_FILE"
+
+ TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false"
+ TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
+
+ FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
+
+ {
+ if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
+ echo "_Keine Datei hat aktuell Duplikate._"
+ echo ""
+ else
+ echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
+ echo ""
+ echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
+ echo "|---|---:|---:|---:|---:|"
+ echo "${TREE_RESP}" | jq -r '
+ .components[]?
+ | . as $c
+ | {
+ path: ($c.path // $c.key),
+ blocks: (($c.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0"),
+ lines: (($c.measures[]? | select(.metric=="duplicated_lines") | .value) // "0"),
+ pct: (($c.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0"),
+ ncloc: (($c.measures[]? | select(.metric=="ncloc") | .value) // "-")
+ }
+ | select((.blocks | tonumber) > 0)
+ | "| `\(.path)` | \(.blocks) | \(.lines) | \(.pct)% | \(.ncloc) |"
+ '
+ echo ""
+ echo "### Ratings & Issues"
+ echo ""
+ echo "| Bewertung | Wert | Anzahl |"
+ echo "|---|---|---|"
+ echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
+ echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
+ echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
+ echo "| Technische Schuld | ${DEBT_FMT} | – |"
+ } >> "$SUMMARY_FILE"
+
+ - name: Fail job on Quality Gate failure
+ if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
+ run: |
+ echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
+ exit 1
\ No newline at end of file
diff --git a/sonar-project.properties b/sonar-project.properties
deleted file mode 100644
index 1b9f265..0000000
--- a/sonar-project.properties
+++ /dev/null
@@ -1,12 +0,0 @@
-sonar.projectKey=LazyCook
-sonar.projectName=LazyCook
-sonar.sourceEncoding=UTF-8
-
-sonar.sources=project/backend,project/frontend
-sonar.tests=project/backend/tests
-sonar.test.inclusions=project/backend/tests/**/*.py
-sonar.python.coverage.reportPaths=project/backend/coverage.xml
-sonar.coverage.exclusions=project/frontend/**,project/backend/tests/**
-
-sonar.exclusions=**/__pycache__/**,project/data/**,project/frontend/.next/**,project/frontend/node_modules/**,project/backend/coverage.xml
-sonar.python.version=3.11
From 34d8b0012324cf7b225f3e216917959f150551be Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 19 May 2026 22:54:55 +0200
Subject: [PATCH 40/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 5 -----
1 file changed, 5 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index efdb4e8..e50656b 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -57,11 +57,6 @@ jobs:
working-directory: project/frontend
run: npm ci
- - name: Run frontend tests with coverage
- working-directory: project/frontend
- run: npm run test:coverage --if-present
- continue-on-error: true
-
# =====================================================
# SONARCLOUD SCAN
# =====================================================
From ad4f4cf07d3424f0f5adb5eb925cb4b1c290862c Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 19 May 2026 23:00:25 +0200
Subject: [PATCH 41/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
project/sonar-project.properties | 32 ++++++++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
create mode 100644 project/sonar-project.properties
diff --git a/project/sonar-project.properties b/project/sonar-project.properties
new file mode 100644
index 0000000..142cea1
--- /dev/null
+++ b/project/sonar-project.properties
@@ -0,0 +1,32 @@
+# SonarCloud / SonarQube Projektkonfiguration
+sonar.projectKey=GalacticCodeGambit_LazyCook
+sonar.organization=galacticcodegambit
+sonar.projectName=LazyCook
+sonar.projectVersion=1.0
+
+# Quellen + Tests
+sonar.sources=backend,frontend/app
+sonar.tests=backend/tests
+sonar.exclusions=**/node_modules/**,**/__pycache__/**,**/.next/**,frontend/app/components/ui/**,**/*.config.*,**/coverage/**,backend/tests/**
+
+# Encoding
+sonar.sourceEncoding=UTF-8
+
+# Python
+sonar.python.version=3.10
+sonar.python.coverage.reportPaths=backend/coverage.xml
+
+# JavaScript/TypeScript
+sonar.javascript.lcov.reportPaths=frontend/coverage/lcov.info
+sonar.typescript.lcov.reportPaths=frontend/coverage/lcov.info
+
+# CPD-Schwellwerte auf das absolute Minimum gestellt:
+# auch kleinste Duplikate (>= 3 Zeilen / >= 10 Tokens) sollen gefunden werden
+sonar.cpd.python.minimumTokens=10
+sonar.cpd.python.minimumLines=3
+sonar.cpd.css.minimumTokens=10
+sonar.cpd.css.minimumLines=3
+sonar.cpd.javascript.minimumTokens=10
+sonar.cpd.javascript.minimumLines=3
+sonar.cpd.typescript.minimumTokens=10
+sonar.cpd.typescript.minimumLines=3
From 1caad8893df6ca1a889677fb2329469685b549a0 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 19 May 2026 23:05:15 +0200
Subject: [PATCH 42/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 30 ++++++++++++++++++++----------
1 file changed, 20 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index e50656b..d89e266 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -57,6 +57,11 @@ jobs:
working-directory: project/frontend
run: npm ci
+ - name: Run frontend tests with coverage
+ working-directory: project/frontend
+ run: npm run test:coverage --if-present
+ continue-on-error: true
+
# =====================================================
# SONARCLOUD SCAN
# =====================================================
@@ -64,6 +69,10 @@ jobs:
uses: SonarSource/sonarqube-scan-action@v3
with:
projectBaseDir: project
+ args: >
+ -Dsonar.projectKey=GalacticCodeGambit_LazyCook
+ -Dsonar.organization=galacticcodegambit
+ -Dproject.settings=sonar-project.properties
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
@@ -202,18 +211,19 @@ jobs:
echo "|---|---:|---:|---:|---:|"
echo "${TREE_RESP}" | jq -r '
.components[]?
- | . as $c
- | {
- path: ($c.path // $c.key),
- blocks: (($c.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0"),
- lines: (($c.measures[]? | select(.metric=="duplicated_lines") | .value) // "0"),
- pct: (($c.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0"),
- ncloc: (($c.measures[]? | select(.metric=="ncloc") | .value) // "-")
- }
- | select((.blocks | tonumber) > 0)
- | "| `\(.path)` | \(.blocks) | \(.lines) | \(.pct)% | \(.ncloc) |"
+ | (.path // .key) as $p
+ | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
+ | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
+ | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
+ | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
+ | select(($b | tonumber) > 0)
+ | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
'
echo ""
+ fi
+ echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
+ echo ""
+
echo "### Ratings & Issues"
echo ""
echo "| Bewertung | Wert | Anzahl |"
From 4d2ee9aa9f4d9177a354d17a8d2a258f3e104214 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 19 May 2026 23:07:09 +0200
Subject: [PATCH 43/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
project/.coveragerc | 15 ++
project/.github/workflows/sonar.yml | 241 ++++++++++++++++++++++++++++
2 files changed, 256 insertions(+)
create mode 100644 project/.coveragerc
create mode 100644 project/.github/workflows/sonar.yml
diff --git a/project/.coveragerc b/project/.coveragerc
new file mode 100644
index 0000000..1757c8b
--- /dev/null
+++ b/project/.coveragerc
@@ -0,0 +1,15 @@
+[run]
+# Pfade in coverage.xml relativ statt absolut – wichtig für Sonar-Mapping
+relative_files = True
+source = backend
+branch = True
+omit =
+ backend/tests/*
+ backend/__pycache__/*
+
+[report]
+show_missing = True
+skip_empty = True
+
+[xml]
+output = coverage.xml
diff --git a/project/.github/workflows/sonar.yml b/project/.github/workflows/sonar.yml
new file mode 100644
index 0000000..4c46886
--- /dev/null
+++ b/project/.github/workflows/sonar.yml
@@ -0,0 +1,241 @@
+name: SonarCloud Analysis
+
+on:
+ push:
+ branches: [main, master, develop]
+ pull_request:
+ branches: [main, master, develop]
+ workflow_dispatch:
+
+jobs:
+ sonarcloud:
+ name: Build, Test & Sonar Scan
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ # =====================================================
+ # BACKEND (Python / FastAPI) – Tests + Coverage
+ # =====================================================
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+
+ - name: Install backend dependencies
+ working-directory: project/backend
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install -r requirements.txt
+ python -m pip install "pytest-cov>=5,<7" "coverage>=7"
+
+ - name: Run backend tests with coverage
+ working-directory: project
+ run: |
+ python -m pytest \
+ --cov=backend \
+ --cov-report=xml:backend/coverage.xml \
+ --cov-report=term-missing \
+ backend/tests/
+ continue-on-error: true
+
+ # =====================================================
+ # FRONTEND (Next.js / TypeScript) – Tests + Coverage
+ # =====================================================
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ cache: "npm"
+ cache-dependency-path: project/frontend/package-lock.json
+
+ - name: Install frontend dependencies
+ working-directory: project/frontend
+ run: npm ci
+
+ - name: Run frontend tests with coverage
+ working-directory: project/frontend
+ run: npm run test:coverage --if-present
+ continue-on-error: true
+
+ # =====================================================
+ # SONARCLOUD SCAN
+ # =====================================================
+ - name: SonarQube Scan
+ uses: SonarSource/sonarqube-scan-action@v3
+ with:
+ projectBaseDir: project
+ args: >
+ -Dsonar.projectKey=GalacticCodeGambit_LazyCook
+ -Dsonar.organization=galacticcodegambit
+ -Dproject.settings=sonar-project.properties
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ - name: SonarCloud Quality Gate check
+ id: sonar-qg
+ uses: SonarSource/sonarqube-quality-gate-action@master
+ timeout-minutes: 5
+ continue-on-error: true
+ with:
+ scanMetadataReportFile: project/.scannerwork/report-task.txt
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ # =====================================================
+ # METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
+ # =====================================================
+ - name: Render SonarCloud metrics to Job Summary
+ if: always()
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_HOST: https://sonarcloud.io
+ QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
+ run: |
+ set +e
+
+ PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
+ | head -1 | cut -d'=' -f2 | tr -d ' \r\n')
+
+ METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
+
+ API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}"
+ HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
+ -u "${SONAR_TOKEN}:" "${API_URL}")
+ RESP=$(cat /tmp/sonar.json)
+
+ HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
+
+ SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
+
+ if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
+ {
+ echo "## SonarCloud Analyse"
+ echo ""
+ echo ":warning: Konnte die Metriken nicht abrufen."
+ echo ""
+ echo "- **HTTP Status:** \`${HTTP_CODE}\`"
+ echo "- **Project Key:** \`${PROJECT_KEY}\`"
+ echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
+ } >> "$SUMMARY_FILE"
+ exit 0
+ fi
+
+ get() {
+ echo "${RESP}" | jq -r --arg k "$1" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end'
+ }
+
+ get_sub() {
+ local path_enc sub_resp
+ path_enc=$(printf '%s' "$1" | sed 's|/|%2F|g')
+ sub_resp=$(curl -s -u "${SONAR_TOKEN}:" \
+ "${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}:${path_enc}&metricKeys=$2")
+ echo "${sub_resp}" | jq -r --arg k "$2" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end' 2>/dev/null || echo "-"
+ }
+
+ rating() {
+ case "$(get "$1")" in
+ 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
+ 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
+ esac
+ }
+
+ DEBT_MIN=$(get sqale_index)
+ if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
+ DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
+ DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
+ else
+ DEBT_FMT="-"
+ fi
+
+ if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
+ QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
+ else
+ QG_ICON="FAILED"; QG_EMOJI=":x:"
+ fi
+
+ {
+ echo "## SonarCloud Analyse – LazyCook"
+ echo ""
+ echo "**Quality Gate:** ${QG_EMOJI} \`${QG_ICON}\` "
+ echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
+ echo ""
+ echo "### Coverage"
+ echo ""
+ echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
+ echo "|---|---|---|---|---|"
+ echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
+ echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
+ echo "| Frontend (TS/JS/CSS) | $(get_sub frontend/app coverage)% | $(get_sub frontend/app line_coverage)% | $(get_sub frontend/app branch_coverage)% | $(get_sub frontend/app ncloc) |"
+ echo ""
+
+ echo "### Duplikate & Komplexität"
+ echo ""
+ echo "| Bereich | Metrik | Wert |"
+ echo "|---|---|---|"
+ echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
+ echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
+ echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
+ echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
+ echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
+ echo ""
+
+ echo "### Alle Dateien mit Duplikaten"
+ echo ""
+ } >> "$SUMMARY_FILE"
+
+ TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false"
+ TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
+
+ FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
+
+ {
+ if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
+ echo "_Keine Datei hat aktuell Duplikate._"
+ echo ""
+ else
+ echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
+ echo ""
+ echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
+ echo "|---|---:|---:|---:|---:|"
+ echo "${TREE_RESP}" | jq -r '
+ .components[]?
+ | (.path // .key) as $p
+ | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
+ | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
+ | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
+ | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
+ | select(($b | tonumber) > 0)
+ | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
+ '
+ echo ""
+ fi
+ echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
+ echo ""
+
+ echo "### Ratings & Issues"
+ echo ""
+ echo "| Bewertung | Wert | Anzahl |"
+ echo "|---|---|---|"
+ echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
+ echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
+ echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
+ echo "| Technische Schuld | ${DEBT_FMT} | – |"
+ } >> "$SUMMARY_FILE"
+
+ - name: Fail job on Quality Gate failure
+ if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
+ run: |
+ echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
+ exit 1
From 641cf9cebbebc9eca97bb9560a4df22ee85a1922 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Wed, 20 May 2026 13:06:07 +0200
Subject: [PATCH 44/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 9 ---------
1 file changed, 9 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index d89e266..1733a2c 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -57,11 +57,6 @@ jobs:
working-directory: project/frontend
run: npm ci
- - name: Run frontend tests with coverage
- working-directory: project/frontend
- run: npm run test:coverage --if-present
- continue-on-error: true
-
# =====================================================
# SONARCLOUD SCAN
# =====================================================
@@ -220,10 +215,6 @@ jobs:
| "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
'
echo ""
- fi
- echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
- echo ""
-
echo "### Ratings & Issues"
echo ""
echo "| Bewertung | Wert | Anzahl |"
From bd34f2b91ff781b9130adc7eb775f93b25045643 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Wed, 20 May 2026 13:10:46 +0200
Subject: [PATCH 45/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 24 ++-
project/.github/workflows/sonar.yml | 241 ----------------------------
2 files changed, 17 insertions(+), 248 deletions(-)
delete mode 100644 project/.github/workflows/sonar.yml
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 1733a2c..27e5e20 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -57,6 +57,11 @@ jobs:
working-directory: project/frontend
run: npm ci
+ - name: Run frontend tests with coverage
+ working-directory: project/frontend
+ run: npm run test:coverage --if-present
+ continue-on-error: true
+
# =====================================================
# SONARCLOUD SCAN
# =====================================================
@@ -114,8 +119,8 @@ jobs:
echo ""
echo ":warning: Konnte die Metriken nicht abrufen."
echo ""
- echo "- **HTTP Status:** \`${HTTP_CODE}\`"
- echo "- **Project Key:** \`${PROJECT_KEY}\`"
+ echo "- **HTTP Status:** ${HTTP_CODE}"
+ echo "- **Project Key:** ${PROJECT_KEY}"
echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
} >> "$SUMMARY_FILE"
exit 0
@@ -129,10 +134,11 @@ jobs:
}
get_sub() {
- local path_enc sub_resp
- path_enc=$(printf '%s' "$1" | sed 's|/|%2F|g')
- sub_resp=$(curl -s -u "${SONAR_TOKEN}:" \
- "${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}:${path_enc}&metricKeys=$2")
+ local sub_resp
+ sub_resp=$(curl -s -u "${SONAR_TOKEN}:" -G \
+ --data-urlencode "component=${PROJECT_KEY}:$1" \
+ --data-urlencode "metricKeys=$2" \
+ "${SONAR_HOST}/api/measures/component")
echo "${sub_resp}" | jq -r --arg k "$2" \
'if (.component? and .component.measures?) then
((.component.measures[] | select(.metric==$k) | .value) // "-")
@@ -163,7 +169,7 @@ jobs:
{
echo "## SonarCloud Analyse – LazyCook"
echo ""
- echo "**Quality Gate:** ${QG_EMOJI} \`${QG_ICON}\` "
+ echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
echo ""
echo "### Coverage"
@@ -215,6 +221,10 @@ jobs:
| "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
'
echo ""
+ fi
+ echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
+ echo ""
+
echo "### Ratings & Issues"
echo ""
echo "| Bewertung | Wert | Anzahl |"
diff --git a/project/.github/workflows/sonar.yml b/project/.github/workflows/sonar.yml
deleted file mode 100644
index 4c46886..0000000
--- a/project/.github/workflows/sonar.yml
+++ /dev/null
@@ -1,241 +0,0 @@
-name: SonarCloud Analysis
-
-on:
- push:
- branches: [main, master, develop]
- pull_request:
- branches: [main, master, develop]
- workflow_dispatch:
-
-jobs:
- sonarcloud:
- name: Build, Test & Sonar Scan
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- # =====================================================
- # BACKEND (Python / FastAPI) – Tests + Coverage
- # =====================================================
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: "3.10"
-
- - name: Install backend dependencies
- working-directory: project/backend
- run: |
- python -m pip install --upgrade pip
- python -m pip install -r requirements.txt
- python -m pip install "pytest-cov>=5,<7" "coverage>=7"
-
- - name: Run backend tests with coverage
- working-directory: project
- run: |
- python -m pytest \
- --cov=backend \
- --cov-report=xml:backend/coverage.xml \
- --cov-report=term-missing \
- backend/tests/
- continue-on-error: true
-
- # =====================================================
- # FRONTEND (Next.js / TypeScript) – Tests + Coverage
- # =====================================================
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: "20"
- cache: "npm"
- cache-dependency-path: project/frontend/package-lock.json
-
- - name: Install frontend dependencies
- working-directory: project/frontend
- run: npm ci
-
- - name: Run frontend tests with coverage
- working-directory: project/frontend
- run: npm run test:coverage --if-present
- continue-on-error: true
-
- # =====================================================
- # SONARCLOUD SCAN
- # =====================================================
- - name: SonarQube Scan
- uses: SonarSource/sonarqube-scan-action@v3
- with:
- projectBaseDir: project
- args: >
- -Dsonar.projectKey=GalacticCodeGambit_LazyCook
- -Dsonar.organization=galacticcodegambit
- -Dproject.settings=sonar-project.properties
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
-
- - name: SonarCloud Quality Gate check
- id: sonar-qg
- uses: SonarSource/sonarqube-quality-gate-action@master
- timeout-minutes: 5
- continue-on-error: true
- with:
- scanMetadataReportFile: project/.scannerwork/report-task.txt
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
-
- # =====================================================
- # METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
- # =====================================================
- - name: Render SonarCloud metrics to Job Summary
- if: always()
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- SONAR_HOST: https://sonarcloud.io
- QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
- run: |
- set +e
-
- PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
- | head -1 | cut -d'=' -f2 | tr -d ' \r\n')
-
- METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
-
- API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}"
- HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
- -u "${SONAR_TOKEN}:" "${API_URL}")
- RESP=$(cat /tmp/sonar.json)
-
- HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
-
- SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
-
- if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
- {
- echo "## SonarCloud Analyse"
- echo ""
- echo ":warning: Konnte die Metriken nicht abrufen."
- echo ""
- echo "- **HTTP Status:** \`${HTTP_CODE}\`"
- echo "- **Project Key:** \`${PROJECT_KEY}\`"
- echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
- } >> "$SUMMARY_FILE"
- exit 0
- fi
-
- get() {
- echo "${RESP}" | jq -r --arg k "$1" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end'
- }
-
- get_sub() {
- local path_enc sub_resp
- path_enc=$(printf '%s' "$1" | sed 's|/|%2F|g')
- sub_resp=$(curl -s -u "${SONAR_TOKEN}:" \
- "${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}:${path_enc}&metricKeys=$2")
- echo "${sub_resp}" | jq -r --arg k "$2" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end' 2>/dev/null || echo "-"
- }
-
- rating() {
- case "$(get "$1")" in
- 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
- 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
- esac
- }
-
- DEBT_MIN=$(get sqale_index)
- if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
- DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
- DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
- else
- DEBT_FMT="-"
- fi
-
- if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
- QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
- else
- QG_ICON="FAILED"; QG_EMOJI=":x:"
- fi
-
- {
- echo "## SonarCloud Analyse – LazyCook"
- echo ""
- echo "**Quality Gate:** ${QG_EMOJI} \`${QG_ICON}\` "
- echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
- echo ""
- echo "### Coverage"
- echo ""
- echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
- echo "|---|---|---|---|---|"
- echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
- echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
- echo "| Frontend (TS/JS/CSS) | $(get_sub frontend/app coverage)% | $(get_sub frontend/app line_coverage)% | $(get_sub frontend/app branch_coverage)% | $(get_sub frontend/app ncloc) |"
- echo ""
-
- echo "### Duplikate & Komplexität"
- echo ""
- echo "| Bereich | Metrik | Wert |"
- echo "|---|---|---|"
- echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
- echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
- echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
- echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
- echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
- echo ""
-
- echo "### Alle Dateien mit Duplikaten"
- echo ""
- } >> "$SUMMARY_FILE"
-
- TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false"
- TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
-
- FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
-
- {
- if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
- echo "_Keine Datei hat aktuell Duplikate._"
- echo ""
- else
- echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
- echo ""
- echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
- echo "|---|---:|---:|---:|---:|"
- echo "${TREE_RESP}" | jq -r '
- .components[]?
- | (.path // .key) as $p
- | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
- | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
- | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
- | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
- | select(($b | tonumber) > 0)
- | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
- '
- echo ""
- fi
- echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
- echo ""
-
- echo "### Ratings & Issues"
- echo ""
- echo "| Bewertung | Wert | Anzahl |"
- echo "|---|---|---|"
- echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
- echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
- echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
- echo "| Technische Schuld | ${DEBT_FMT} | – |"
- } >> "$SUMMARY_FILE"
-
- - name: Fail job on Quality Gate failure
- if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
- run: |
- echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
- exit 1
From a749a1ed5e5a5e863b851d52aab19008819d2270 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Wed, 20 May 2026 13:21:27 +0200
Subject: [PATCH 46/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
project/.coveragerc | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/project/.coveragerc b/project/.coveragerc
index 1757c8b..033ec32 100644
--- a/project/.coveragerc
+++ b/project/.coveragerc
@@ -1,7 +1,7 @@
[run]
-# Pfade in coverage.xml relativ statt absolut – wichtig für Sonar-Mapping
+# Pfade in coverage.xml relativ zum Working-Dir (= project/)
+# damit Sonar "backend/Auth.py" matcht und nicht nur "Auth.py"
relative_files = True
-source = backend
branch = True
omit =
backend/tests/*
@@ -10,6 +10,3 @@ omit =
[report]
show_missing = True
skip_empty = True
-
-[xml]
-output = coverage.xml
From 491a4183dd6ee3822a812e850f6068aee672ded7 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Wed, 20 May 2026 13:28:59 +0200
Subject: [PATCH 47/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 27e5e20..06dc9e8 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -73,9 +73,13 @@ jobs:
-Dsonar.projectKey=GalacticCodeGambit_LazyCook
-Dsonar.organization=galacticcodegambit
-Dproject.settings=sonar-project.properties
+ -Dsonar.python.coverage.reportPaths=backend/coverage.xml
+ -Dsonar.javascript.lcov.reportPaths=frontend/coverage/lcov.info
+ -Dsonar.typescript.lcov.reportPaths=frontend/coverage/lcov.info
+ -Dsonar.python.version=3.10
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: SonarCloud Quality Gate check
id: sonar-qg
From 9237ab7be252506421e81ac968b1603e33603110 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Wed, 20 May 2026 13:33:24 +0200
Subject: [PATCH 48/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 42 ++++++++++++++------------------
project/sonar-project.properties | 4 ---
2 files changed, 18 insertions(+), 28 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 06dc9e8..717110d 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -69,17 +69,9 @@ jobs:
uses: SonarSource/sonarqube-scan-action@v3
with:
projectBaseDir: project
- args: >
- -Dsonar.projectKey=GalacticCodeGambit_LazyCook
- -Dsonar.organization=galacticcodegambit
- -Dproject.settings=sonar-project.properties
- -Dsonar.python.coverage.reportPaths=backend/coverage.xml
- -Dsonar.javascript.lcov.reportPaths=frontend/coverage/lcov.info
- -Dsonar.typescript.lcov.reportPaths=frontend/coverage/lcov.info
- -Dsonar.python.version=3.10
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: SonarCloud Quality Gate check
id: sonar-qg
@@ -123,8 +115,8 @@ jobs:
echo ""
echo ":warning: Konnte die Metriken nicht abrufen."
echo ""
- echo "- **HTTP Status:** ${HTTP_CODE}"
- echo "- **Project Key:** ${PROJECT_KEY}"
+ echo "- **HTTP Status:** \`${HTTP_CODE}\`"
+ echo "- **Project Key:** \`${PROJECT_KEY}\`"
echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
} >> "$SUMMARY_FILE"
exit 0
@@ -138,11 +130,10 @@ jobs:
}
get_sub() {
- local sub_resp
- sub_resp=$(curl -s -u "${SONAR_TOKEN}:" -G \
- --data-urlencode "component=${PROJECT_KEY}:$1" \
- --data-urlencode "metricKeys=$2" \
- "${SONAR_HOST}/api/measures/component")
+ local path_enc sub_resp
+ path_enc=$(printf '%s' "$1" | sed 's|/|%2F|g')
+ sub_resp=$(curl -s -u "${SONAR_TOKEN}:" \
+ "${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}:${path_enc}&metricKeys=$2")
echo "${sub_resp}" | jq -r --arg k "$2" \
'if (.component? and .component.measures?) then
((.component.measures[] | select(.metric==$k) | .value) // "-")
@@ -173,7 +164,7 @@ jobs:
{
echo "## SonarCloud Analyse – LazyCook"
echo ""
- echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
+ echo "**Quality Gate:** ${QG_EMOJI} \`${QG_ICON}\` "
echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
echo ""
echo "### Coverage"
@@ -216,13 +207,16 @@ jobs:
echo "|---|---:|---:|---:|---:|"
echo "${TREE_RESP}" | jq -r '
.components[]?
- | (.path // .key) as $p
- | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
- | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
- | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
- | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
- | select(($b | tonumber) > 0)
- | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
+ | . as $c
+ | {
+ path: ($c.path // $c.key),
+ blocks: (($c.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0"),
+ lines: (($c.measures[]? | select(.metric=="duplicated_lines") | .value) // "0"),
+ pct: (($c.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0"),
+ ncloc: (($c.measures[]? | select(.metric=="ncloc") | .value) // "-")
+ }
+ | select((.blocks | tonumber) > 0)
+ | "| `\(.path)` | \(.blocks) | \(.lines) | \(.pct)% | \(.ncloc) |"
'
echo ""
fi
diff --git a/project/sonar-project.properties b/project/sonar-project.properties
index 142cea1..78c8d4b 100644
--- a/project/sonar-project.properties
+++ b/project/sonar-project.properties
@@ -16,10 +16,6 @@ sonar.sourceEncoding=UTF-8
sonar.python.version=3.10
sonar.python.coverage.reportPaths=backend/coverage.xml
-# JavaScript/TypeScript
-sonar.javascript.lcov.reportPaths=frontend/coverage/lcov.info
-sonar.typescript.lcov.reportPaths=frontend/coverage/lcov.info
-
# CPD-Schwellwerte auf das absolute Minimum gestellt:
# auch kleinste Duplikate (>= 3 Zeilen / >= 10 Tokens) sollen gefunden werden
sonar.cpd.python.minimumTokens=10
From 6b0310e54e56c5ea630b9f39adf2a63a4b84850d Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Wed, 20 May 2026 13:37:46 +0200
Subject: [PATCH 49/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 20 --------------------
1 file changed, 20 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 717110d..5844e07 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -43,25 +43,6 @@ jobs:
backend/tests/
continue-on-error: true
- # =====================================================
- # FRONTEND (Next.js / TypeScript) – Tests + Coverage
- # =====================================================
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: "20"
- cache: "npm"
- cache-dependency-path: project/frontend/package-lock.json
-
- - name: Install frontend dependencies
- working-directory: project/frontend
- run: npm ci
-
- - name: Run frontend tests with coverage
- working-directory: project/frontend
- run: npm run test:coverage --if-present
- continue-on-error: true
-
# =====================================================
# SONARCLOUD SCAN
# =====================================================
@@ -173,7 +154,6 @@ jobs:
echo "|---|---|---|---|---|"
echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
- echo "| Frontend (TS/JS/CSS) | $(get_sub frontend/app coverage)% | $(get_sub frontend/app line_coverage)% | $(get_sub frontend/app branch_coverage)% | $(get_sub frontend/app ncloc) |"
echo ""
echo "### Duplikate & Komplexität"
From a88dbe468fb90fa00a65d77343dbe3323bab13d2 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Wed, 20 May 2026 13:43:27 +0200
Subject: [PATCH 50/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 38 ++++++++++++++++++---------------
1 file changed, 21 insertions(+), 17 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 5844e07..fb8456a 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -50,6 +50,12 @@ jobs:
uses: SonarSource/sonarqube-scan-action@v3
with:
projectBaseDir: project
+ args: >
+ -Dsonar.projectKey=GalacticCodeGambit_LazyCook
+ -Dsonar.organization=galacticcodegambit
+ -Dproject.settings=sonar-project.properties
+ -Dsonar.python.coverage.reportPaths=backend/coverage.xml
+ -Dsonar.python.version=3.10
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
@@ -96,8 +102,8 @@ jobs:
echo ""
echo ":warning: Konnte die Metriken nicht abrufen."
echo ""
- echo "- **HTTP Status:** \`${HTTP_CODE}\`"
- echo "- **Project Key:** \`${PROJECT_KEY}\`"
+ echo "- **HTTP Status:** ${HTTP_CODE}"
+ echo "- **Project Key:** ${PROJECT_KEY}"
echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
} >> "$SUMMARY_FILE"
exit 0
@@ -111,10 +117,11 @@ jobs:
}
get_sub() {
- local path_enc sub_resp
- path_enc=$(printf '%s' "$1" | sed 's|/|%2F|g')
- sub_resp=$(curl -s -u "${SONAR_TOKEN}:" \
- "${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}:${path_enc}&metricKeys=$2")
+ local sub_resp
+ sub_resp=$(curl -s -u "${SONAR_TOKEN}:" -G \
+ --data-urlencode "component=${PROJECT_KEY}:$1" \
+ --data-urlencode "metricKeys=$2" \
+ "${SONAR_HOST}/api/measures/component")
echo "${sub_resp}" | jq -r --arg k "$2" \
'if (.component? and .component.measures?) then
((.component.measures[] | select(.metric==$k) | .value) // "-")
@@ -145,7 +152,7 @@ jobs:
{
echo "## SonarCloud Analyse – LazyCook"
echo ""
- echo "**Quality Gate:** ${QG_EMOJI} \`${QG_ICON}\` "
+ echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
echo ""
echo "### Coverage"
@@ -187,16 +194,13 @@ jobs:
echo "|---|---:|---:|---:|---:|"
echo "${TREE_RESP}" | jq -r '
.components[]?
- | . as $c
- | {
- path: ($c.path // $c.key),
- blocks: (($c.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0"),
- lines: (($c.measures[]? | select(.metric=="duplicated_lines") | .value) // "0"),
- pct: (($c.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0"),
- ncloc: (($c.measures[]? | select(.metric=="ncloc") | .value) // "-")
- }
- | select((.blocks | tonumber) > 0)
- | "| `\(.path)` | \(.blocks) | \(.lines) | \(.pct)% | \(.ncloc) |"
+ | (.path // .key) as $p
+ | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
+ | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
+ | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
+ | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
+ | select(($b | tonumber) > 0)
+ | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
'
echo ""
fi
From 34b75f7cee84289f727f7756b94a25720e783917 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Wed, 20 May 2026 13:51:44 +0200
Subject: [PATCH 51/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 49 ++++++++++++++++++++++++++++-----
1 file changed, 42 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index fb8456a..932cf02 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -79,21 +79,42 @@ jobs:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST: https://sonarcloud.io
QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set +e
PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
| head -1 | cut -d'=' -f2 | tr -d ' \r\n')
+ # PR-Kontext erkennen: dann Daten der PR-Analyse holen, nicht der Main-Branch
+ PR_PARAM=""
+ if [ -n "${PR_NUMBER}" ]; then
+ PR_PARAM="&pullRequest=${PR_NUMBER}"
+ echo "PR-Modus aktiv – Daten werden für PR #${PR_NUMBER} geholt"
+ else
+ echo "Branch-Modus aktiv – Daten werden für den Default-Branch geholt"
+ fi
+
METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
- API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}"
+ API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}${PR_PARAM}"
HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
-u "${SONAR_TOKEN}:" "${API_URL}")
RESP=$(cat /tmp/sonar.json)
HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
+ # DIAGNOSE – wir wollen sehen, was der Script konkret sieht
+ echo "===== DEBUG ====="
+ echo "PROJECT_KEY = '${PROJECT_KEY}'"
+ echo "API_URL = ${API_URL}"
+ echo "HTTP_CODE = ${HTTP_CODE}"
+ echo "HAS_MEASURES= ${HAS_MEASURES}"
+ echo "RESP first 800 chars:"
+ echo "${RESP}" | head -c 800
+ echo ""
+ echo "===== /DEBUG ====="
+
SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
@@ -117,11 +138,14 @@ jobs:
}
get_sub() {
- local sub_resp
- sub_resp=$(curl -s -u "${SONAR_TOKEN}:" -G \
- --data-urlencode "component=${PROJECT_KEY}:$1" \
- --data-urlencode "metricKeys=$2" \
- "${SONAR_HOST}/api/measures/component")
+ local sub_resp curl_args
+ curl_args=(-s -u "${SONAR_TOKEN}:" -G
+ --data-urlencode "component=${PROJECT_KEY}:$1"
+ --data-urlencode "metricKeys=$2")
+ if [ -n "${PR_NUMBER}" ]; then
+ curl_args+=(--data-urlencode "pullRequest=${PR_NUMBER}")
+ fi
+ sub_resp=$(curl "${curl_args[@]}" "${SONAR_HOST}/api/measures/component")
echo "${sub_resp}" | jq -r --arg k "$2" \
'if (.component? and .component.measures?) then
((.component.measures[] | select(.metric==$k) | .value) // "-")
@@ -155,6 +179,17 @@ jobs:
echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
echo ""
+ echo "Debug-Info (kann später raus)
"
+ echo ""
+ echo "- PROJECT_KEY: ${PROJECT_KEY}"
+ echo "- HTTP_CODE: ${HTTP_CODE}"
+ echo "- HAS_MEASURES: ${HAS_MEASURES}"
+ echo "- coverage value direkt aus get: '$(get coverage)'"
+ echo "- ncloc direkt aus get: '$(get ncloc)'"
+ echo "- duplicated_lines_density direkt aus get: '$(get duplicated_lines_density)'"
+ echo ""
+ echo " "
+ echo ""
echo "### Coverage"
echo ""
echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
@@ -178,7 +213,7 @@ jobs:
echo ""
} >> "$SUMMARY_FILE"
- TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false"
+ TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false${PR_PARAM}"
TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
From c7647ced34c2d0d37108e44425184fb420d560d5 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Wed, 20 May 2026 13:55:25 +0200
Subject: [PATCH 52/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 22 ----------------------
1 file changed, 22 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 932cf02..bf6704a 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -104,17 +104,6 @@ jobs:
HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
- # DIAGNOSE – wir wollen sehen, was der Script konkret sieht
- echo "===== DEBUG ====="
- echo "PROJECT_KEY = '${PROJECT_KEY}'"
- echo "API_URL = ${API_URL}"
- echo "HTTP_CODE = ${HTTP_CODE}"
- echo "HAS_MEASURES= ${HAS_MEASURES}"
- echo "RESP first 800 chars:"
- echo "${RESP}" | head -c 800
- echo ""
- echo "===== /DEBUG ====="
-
SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
@@ -179,17 +168,6 @@ jobs:
echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
echo ""
- echo "Debug-Info (kann später raus)
"
- echo ""
- echo "- PROJECT_KEY: ${PROJECT_KEY}"
- echo "- HTTP_CODE: ${HTTP_CODE}"
- echo "- HAS_MEASURES: ${HAS_MEASURES}"
- echo "- coverage value direkt aus get: '$(get coverage)'"
- echo "- ncloc direkt aus get: '$(get ncloc)'"
- echo "- duplicated_lines_density direkt aus get: '$(get duplicated_lines_density)'"
- echo ""
- echo " "
- echo ""
echo "### Coverage"
echo ""
echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
From 0e2bc05bad1490a105cb842ae8f67079ecad896a Mon Sep 17 00:00:00 2001
From: F <150372421+GalacticCodeGambit@users.noreply.github.com>
Date: Wed, 20 May 2026 18:12:17 +0200
Subject: [PATCH 53/85] Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
.github/workflows/sonarqube.yml | 3 +++
docs/Review-Protokoll-01.md | 2 +-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index bf6704a..b08c7cd 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -7,6 +7,9 @@ on:
branches: [main, master, develop]
workflow_dispatch:
+permissions:
+ contents: read
+
jobs:
sonarcloud:
name: Build, Test & Sonar Scan
diff --git a/docs/Review-Protokoll-01.md b/docs/Review-Protokoll-01.md
index 5ce09da..d49bd59 100644
--- a/docs/Review-Protokoll-01.md
+++ b/docs/Review-Protokoll-01.md
@@ -1,6 +1,6 @@
# Review-Protokoll-01
### **Datum** (Startzeit und Endzeit):
-12.05.2026 16.:00 bis 16:20
+12.05.2026 16:00 bis 16:20
### **Teilnehmende**:
- Moderator: PrussianBaron
From 154afd38dfe76e03eeef8feb16cd5073c18f4faf Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 08:42:45 +0200
Subject: [PATCH 54/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 215 +++-----------------------------
1 file changed, 15 insertions(+), 200 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index bf6704a..5494497 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -9,43 +9,27 @@ on:
jobs:
sonarcloud:
- name: Build, Test & Sonar Scan
+ name: Sonar Scan
runs-on: ubuntu-latest
steps:
- - name: Checkout repository
- uses: actions/checkout@v4
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
- # =====================================================
- # BACKEND (Python / FastAPI) – Tests + Coverage
- # =====================================================
- - name: Set up Python
- uses: actions/setup-python@v5
+ - uses: actions/setup-python@v5
with:
python-version: "3.10"
- - name: Install backend dependencies
- working-directory: project/backend
- run: |
- python -m pip install --upgrade pip
- python -m pip install -r requirements.txt
- python -m pip install "pytest-cov>=5,<7" "coverage>=7"
-
- - name: Run backend tests with coverage
+ - name: Install deps & run tests with coverage
working-directory: project
run: |
- python -m pytest \
- --cov=backend \
- --cov-report=xml:backend/coverage.xml \
- --cov-report=term-missing \
- backend/tests/
+ python -m pip install --upgrade pip
+ python -m pip install -r backend/requirements.txt
+ python -m pip install "pytest-cov>=5,<7"
+ python -m pytest --cov=backend --cov-report=xml:backend/coverage.xml backend/tests/
continue-on-error: true
- # =====================================================
- # SONARCLOUD SCAN
- # =====================================================
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v3
with:
@@ -53,185 +37,16 @@ jobs:
args: >
-Dsonar.projectKey=GalacticCodeGambit_LazyCook
-Dsonar.organization=galacticcodegambit
- -Dproject.settings=sonar-project.properties
-Dsonar.python.coverage.reportPaths=backend/coverage.xml
-Dsonar.python.version=3.10
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
-
- - name: SonarCloud Quality Gate check
- id: sonar-qg
- uses: SonarSource/sonarqube-quality-gate-action@master
- timeout-minutes: 5
- continue-on-error: true
- with:
- scanMetadataReportFile: project/.scannerwork/report-task.txt
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- # =====================================================
- # METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
- # =====================================================
- - name: Render SonarCloud metrics to Job Summary
+ - name: Quality Gate + Summary-Tabelle
if: always()
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- SONAR_HOST: https://sonarcloud.io
- QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
- PR_NUMBER: ${{ github.event.pull_request.number }}
- run: |
- set +e
-
- PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
- | head -1 | cut -d'=' -f2 | tr -d ' \r\n')
-
- # PR-Kontext erkennen: dann Daten der PR-Analyse holen, nicht der Main-Branch
- PR_PARAM=""
- if [ -n "${PR_NUMBER}" ]; then
- PR_PARAM="&pullRequest=${PR_NUMBER}"
- echo "PR-Modus aktiv – Daten werden für PR #${PR_NUMBER} geholt"
- else
- echo "Branch-Modus aktiv – Daten werden für den Default-Branch geholt"
- fi
-
- METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
-
- API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}${PR_PARAM}"
- HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
- -u "${SONAR_TOKEN}:" "${API_URL}")
- RESP=$(cat /tmp/sonar.json)
-
- HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
-
- SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
-
- if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
- {
- echo "## SonarCloud Analyse"
- echo ""
- echo ":warning: Konnte die Metriken nicht abrufen."
- echo ""
- echo "- **HTTP Status:** ${HTTP_CODE}"
- echo "- **Project Key:** ${PROJECT_KEY}"
- echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
- } >> "$SUMMARY_FILE"
- exit 0
- fi
-
- get() {
- echo "${RESP}" | jq -r --arg k "$1" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end'
- }
-
- get_sub() {
- local sub_resp curl_args
- curl_args=(-s -u "${SONAR_TOKEN}:" -G
- --data-urlencode "component=${PROJECT_KEY}:$1"
- --data-urlencode "metricKeys=$2")
- if [ -n "${PR_NUMBER}" ]; then
- curl_args+=(--data-urlencode "pullRequest=${PR_NUMBER}")
- fi
- sub_resp=$(curl "${curl_args[@]}" "${SONAR_HOST}/api/measures/component")
- echo "${sub_resp}" | jq -r --arg k "$2" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end' 2>/dev/null || echo "-"
- }
-
- rating() {
- case "$(get "$1")" in
- 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
- 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
- esac
- }
-
- DEBT_MIN=$(get sqale_index)
- if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
- DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
- DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
- else
- DEBT_FMT="-"
- fi
-
- if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
- QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
- else
- QG_ICON="FAILED"; QG_EMOJI=":x:"
- fi
-
- {
- echo "## SonarCloud Analyse – LazyCook"
- echo ""
- echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
- echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
- echo ""
- echo "### Coverage"
- echo ""
- echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
- echo "|---|---|---|---|---|"
- echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
- echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
- echo ""
-
- echo "### Duplikate & Komplexität"
- echo ""
- echo "| Bereich | Metrik | Wert |"
- echo "|---|---|---|"
- echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
- echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
- echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
- echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
- echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
- echo ""
-
- echo "### Alle Dateien mit Duplikaten"
- echo ""
- } >> "$SUMMARY_FILE"
-
- TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false${PR_PARAM}"
- TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
-
- FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
-
- {
- if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
- echo "_Keine Datei hat aktuell Duplikate._"
- echo ""
- else
- echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
- echo ""
- echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
- echo "|---|---:|---:|---:|---:|"
- echo "${TREE_RESP}" | jq -r '
- .components[]?
- | (.path // .key) as $p
- | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
- | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
- | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
- | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
- | select(($b | tonumber) > 0)
- | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
- '
- echo ""
- fi
- echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
- echo ""
-
- echo "### Ratings & Issues"
- echo ""
- echo "| Bewertung | Wert | Anzahl |"
- echo "|---|---|---|"
- echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
- echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
- echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
- echo "| Technische Schuld | ${DEBT_FMT} | – |"
- } >> "$SUMMARY_FILE"
-
- - name: Fail job on Quality Gate failure
- if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
- run: |
- echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
- exit 1
\ No newline at end of file
+ uses: phwt/sonarqube-quality-gate-action@v1
+ with:
+ sonar-host-url: https://sonarcloud.io
+ sonar-project-key: GalacticCodeGambit_LazyCook
+ sonar-token: ${{ secrets.SONAR_TOKEN }}
+ fail-on-quality-gate-error: "true"
From 36a4e6ad84e8bbaedb457fdd10f3776f52c8446d Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 08:49:00 +0200
Subject: [PATCH 55/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index c961455..332ca69 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -9,6 +9,8 @@ on:
permissions:
contents: read
+ pull-requests: write
+ issues: write
jobs:
sonarcloud:
@@ -52,4 +54,5 @@ jobs:
sonar-host-url: https://sonarcloud.io
sonar-project-key: GalacticCodeGambit_LazyCook
sonar-token: ${{ secrets.SONAR_TOKEN }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
fail-on-quality-gate-error: "true"
From 521c7cdbf5d26eb2cac637a8628aaf0b9d4b949e Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 08:53:19 +0200
Subject: [PATCH 56/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 332ca69..6396def 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -47,6 +47,16 @@ jobs:
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ # 1. Warten bis SonarCloud die Analyse fertig verarbeitet hat
+ - name: Wait for SonarCloud analysis
+ uses: SonarSource/sonarqube-quality-gate-action@master
+ continue-on-error: true
+ with:
+ scanMetadataReportFile: project/.scannerwork/report-task.txt
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ # 2. Tabelle ins Job-Summary + PR-Kommentar schreiben
- name: Quality Gate + Summary-Tabelle
if: always()
uses: phwt/sonarqube-quality-gate-action@v1
@@ -55,4 +65,5 @@ jobs:
sonar-project-key: GalacticCodeGambit_LazyCook
sonar-token: ${{ secrets.SONAR_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
+ working-dir: project
fail-on-quality-gate-error: "true"
From 27c7583c6f67ef57d32c653d2ce544c084b7fe07 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 08:57:21 +0200
Subject: [PATCH 57/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 6396def..118a3a8 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -66,4 +66,7 @@ jobs:
sonar-token: ${{ secrets.SONAR_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
working-dir: project
+ # Explizit Branch ODER PR mitgeben, damit der API-Query den richtigen Scope trifft
+ branch: ${{ github.event_name != 'pull_request' && github.ref_name || '' }}
+ pull-request: ${{ github.event.pull_request.number }}
fail-on-quality-gate-error: "true"
From 5d2f75c9b5fe3f2437322578249a625fb825c52b Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 09:05:19 +0200
Subject: [PATCH 58/85] Test: Sonarqube workflow with result print in
CI-Pipeline
---
.github/workflows/sonarqube.yml | 58 +++++++++++++++++++++++++++++++--
1 file changed, 56 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 118a3a8..28eb8f4 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -56,7 +56,7 @@ jobs:
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- # 2. Tabelle ins Job-Summary + PR-Kommentar schreiben
+ # 2. Quality-Gate-Tabelle ins Job-Summary + PR-Kommentar
- name: Quality Gate + Summary-Tabelle
if: always()
uses: phwt/sonarqube-quality-gate-action@v1
@@ -66,7 +66,61 @@ jobs:
sonar-token: ${{ secrets.SONAR_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
working-dir: project
- # Explizit Branch ODER PR mitgeben, damit der API-Query den richtigen Scope trifft
branch: ${{ github.event_name != 'pull_request' && github.ref_name || '' }}
pull-request: ${{ github.event.pull_request.number }}
fail-on-quality-gate-error: "true"
+
+ # 3. Zusatz-Metriken (Coverage, Duplication, Komplexität) ans Summary anhängen
+ - name: Zusatz-Metriken
+ if: always()
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ PR: ${{ github.event.pull_request.number }}
+ run: |
+ set +e
+ KEY="GalacticCodeGambit_LazyCook"
+ HOST="https://sonarcloud.io"
+ METRICS="coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc"
+
+ # API-Aufruf mit korrektem Scope (PR vs Branch)
+ if [ -n "$PR" ]; then
+ URL="$HOST/api/measures/component?component=$KEY&metricKeys=$METRICS&pullRequest=$PR"
+ else
+ URL="$HOST/api/measures/component?component=$KEY&metricKeys=$METRICS"
+ fi
+ RESP=$(curl -s -u "$SONAR_TOKEN:" "$URL")
+
+ # Helper: Metrik-Wert ziehen, "-" als Default
+ val() {
+ echo "$RESP" | jq -r --arg k "$1" \
+ '(.component.measures[]? | select(.metric==$k) | .value) // "-"' 2>/dev/null
+ }
+
+ {
+ echo ""
+ echo "## Zusatz-Metriken"
+ echo ""
+ echo "### Coverage"
+ echo ""
+ echo "| Metrik | Wert |"
+ echo "|---|---|"
+ echo "| Gesamt-Coverage | $(val coverage)% |"
+ echo "| Line Coverage | $(val line_coverage)% |"
+ echo "| Branch Coverage | $(val branch_coverage)% |"
+ echo "| Lines of Code | $(val ncloc) |"
+ echo ""
+ echo "### Duplikate"
+ echo ""
+ echo "| Metrik | Wert |"
+ echo "|---|---|"
+ echo "| Duplizierte Zeilen (Rate) | $(val duplicated_lines_density)% |"
+ echo "| Duplikat-Blöcke | $(val duplicated_blocks) |"
+ echo "| Betroffene Dateien | $(val duplicated_files) |"
+ echo ""
+ echo "### Komplexität"
+ echo ""
+ echo "| Metrik | Wert |"
+ echo "|---|---|"
+ echo "| Zyklomatische Komplexität | $(val complexity) |"
+ echo "| Cognitive Complexity | $(val cognitive_complexity) |"
+ } >> "$GITHUB_STEP_SUMMARY"
From 1eefcae144b8fea595663721aa1db66727b6b794 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 09:14:38 +0200
Subject: [PATCH 59/85] Test: Bash Print in CI/CD - Pipeline with detailed
information about coverage, duplication and complexity
---
.github/workflows/sonarqube.yml | 241 ++++++++++++++++++++++++--------
1 file changed, 179 insertions(+), 62 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 28eb8f4..a16eea5 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -9,32 +9,46 @@ on:
permissions:
contents: read
- pull-requests: write
- issues: write
jobs:
sonarcloud:
- name: Sonar Scan
+ name: Build, Test & Sonar Scan
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - name: Checkout repository
+ uses: actions/checkout@v4
with:
fetch-depth: 0
- - uses: actions/setup-python@v5
+ # =====================================================
+ # BACKEND (Python / FastAPI) – Tests + Coverage
+ # =====================================================
+ - name: Set up Python
+ uses: actions/setup-python@v5
with:
python-version: "3.10"
- - name: Install deps & run tests with coverage
- working-directory: project
+ - name: Install backend dependencies
+ working-directory: project/backend
run: |
python -m pip install --upgrade pip
- python -m pip install -r backend/requirements.txt
- python -m pip install "pytest-cov>=5,<7"
- python -m pytest --cov=backend --cov-report=xml:backend/coverage.xml backend/tests/
+ python -m pip install -r requirements.txt
+ python -m pip install "pytest-cov>=5,<7" "coverage>=7"
+
+ - name: Run backend tests with coverage
+ working-directory: project
+ run: |
+ python -m pytest \
+ --cov=backend \
+ --cov-report=xml:backend/coverage.xml \
+ --cov-report=term-missing \
+ backend/tests/
continue-on-error: true
+ # =====================================================
+ # SONARCLOUD SCAN
+ # =====================================================
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v3
with:
@@ -42,85 +56,188 @@ jobs:
args: >
-Dsonar.projectKey=GalacticCodeGambit_LazyCook
-Dsonar.organization=galacticcodegambit
+ -Dproject.settings=sonar-project.properties
-Dsonar.python.coverage.reportPaths=backend/coverage.xml
-Dsonar.python.version=3.10
env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- # 1. Warten bis SonarCloud die Analyse fertig verarbeitet hat
- - name: Wait for SonarCloud analysis
+ - name: SonarCloud Quality Gate check
+ id: sonar-qg
uses: SonarSource/sonarqube-quality-gate-action@master
+ timeout-minutes: 5
continue-on-error: true
with:
scanMetadataReportFile: project/.scannerwork/report-task.txt
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- # 2. Quality-Gate-Tabelle ins Job-Summary + PR-Kommentar
- - name: Quality Gate + Summary-Tabelle
- if: always()
- uses: phwt/sonarqube-quality-gate-action@v1
- with:
- sonar-host-url: https://sonarcloud.io
- sonar-project-key: GalacticCodeGambit_LazyCook
- sonar-token: ${{ secrets.SONAR_TOKEN }}
- github-token: ${{ secrets.GITHUB_TOKEN }}
- working-dir: project
- branch: ${{ github.event_name != 'pull_request' && github.ref_name || '' }}
- pull-request: ${{ github.event.pull_request.number }}
- fail-on-quality-gate-error: "true"
-
- # 3. Zusatz-Metriken (Coverage, Duplication, Komplexität) ans Summary anhängen
- - name: Zusatz-Metriken
+ # =====================================================
+ # METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
+ # =====================================================
+ - name: Render SonarCloud metrics to Job Summary
if: always()
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- PR: ${{ github.event.pull_request.number }}
+ SONAR_HOST: https://sonarcloud.io
+ QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set +e
- KEY="GalacticCodeGambit_LazyCook"
- HOST="https://sonarcloud.io"
- METRICS="coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc"
- # API-Aufruf mit korrektem Scope (PR vs Branch)
- if [ -n "$PR" ]; then
- URL="$HOST/api/measures/component?component=$KEY&metricKeys=$METRICS&pullRequest=$PR"
- else
- URL="$HOST/api/measures/component?component=$KEY&metricKeys=$METRICS"
+ PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
+ | head -1 | cut -d'=' -f2 | tr -d ' \r\n')
+
+ # PR-Kontext erkennen: Daten der PR-Analyse statt der Main-Branch holen
+ PR_PARAM=""
+ if [ -n "${PR_NUMBER}" ]; then
+ PR_PARAM="&pullRequest=${PR_NUMBER}"
fi
- RESP=$(curl -s -u "$SONAR_TOKEN:" "$URL")
- # Helper: Metrik-Wert ziehen, "-" als Default
- val() {
- echo "$RESP" | jq -r --arg k "$1" \
- '(.component.measures[]? | select(.metric==$k) | .value) // "-"' 2>/dev/null
+ METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
+
+ API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}${PR_PARAM}"
+ HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
+ -u "${SONAR_TOKEN}:" "${API_URL}")
+ RESP=$(cat /tmp/sonar.json)
+
+ HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
+
+ SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
+
+ if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
+ {
+ echo "## SonarCloud Analyse"
+ echo ""
+ echo ":warning: Konnte die Metriken nicht abrufen."
+ echo ""
+ echo "- **HTTP Status:** ${HTTP_CODE}"
+ echo "- **Project Key:** ${PROJECT_KEY}"
+ echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
+ } >> "$SUMMARY_FILE"
+ exit 0
+ fi
+
+ # Hilfsfunktion: Metrik-Wert ziehen, "-" als Default
+ get() {
+ echo "${RESP}" | jq -r --arg k "$1" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end'
+ }
+
+ # Sub-Komponente abfragen (z.B. backend, frontend/app)
+ get_sub() {
+ local sub_resp curl_args
+ curl_args=(-s -u "${SONAR_TOKEN}:" -G
+ --data-urlencode "component=${PROJECT_KEY}:$1"
+ --data-urlencode "metricKeys=$2")
+ if [ -n "${PR_NUMBER}" ]; then
+ curl_args+=(--data-urlencode "pullRequest=${PR_NUMBER}")
+ fi
+ sub_resp=$(curl "${curl_args[@]}" "${SONAR_HOST}/api/measures/component")
+ echo "${sub_resp}" | jq -r --arg k "$2" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end' 2>/dev/null || echo "-"
+ }
+
+ # Rating-Buchstaben (1.0=A ... 5.0=E)
+ rating() {
+ case "$(get "$1")" in
+ 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
+ 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
+ esac
}
+ # Technical Debt in Stunden + Minuten formatieren
+ DEBT_MIN=$(get sqale_index)
+ if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
+ DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
+ DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
+ else
+ DEBT_FMT="-"
+ fi
+
+ # Quality-Gate-Status
+ if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
+ QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
+ else
+ QG_ICON="FAILED"; QG_EMOJI=":x:"
+ fi
+
{
+ echo "## SonarCloud Analyse – LazyCook"
echo ""
- echo "## Zusatz-Metriken"
+ echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
+ echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
echo ""
echo "### Coverage"
echo ""
- echo "| Metrik | Wert |"
- echo "|---|---|"
- echo "| Gesamt-Coverage | $(val coverage)% |"
- echo "| Line Coverage | $(val line_coverage)% |"
- echo "| Branch Coverage | $(val branch_coverage)% |"
- echo "| Lines of Code | $(val ncloc) |"
+ echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
+ echo "|---|---|---|---|---|"
+ echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
+ echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
+ echo ""
+
+ echo "### Duplikate & Komplexität"
+ echo ""
+ echo "| Bereich | Metrik | Wert |"
+ echo "|---|---|---|"
+ echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
+ echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
+ echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
+ echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
+ echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
echo ""
- echo "### Duplikate"
+
+ echo "### Alle Dateien mit Duplikaten"
echo ""
- echo "| Metrik | Wert |"
- echo "|---|---|"
- echo "| Duplizierte Zeilen (Rate) | $(val duplicated_lines_density)% |"
- echo "| Duplikat-Blöcke | $(val duplicated_blocks) |"
- echo "| Betroffene Dateien | $(val duplicated_files) |"
+ } >> "$SUMMARY_FILE"
+
+ # Datei-Tree mit Duplikaten holen
+ TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false${PR_PARAM}"
+ TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
+
+ FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
+
+ {
+ if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
+ echo "_Keine Datei hat aktuell Duplikate._"
+ echo ""
+ else
+ echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
+ echo ""
+ echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
+ echo "|---|---:|---:|---:|---:|"
+ echo "${TREE_RESP}" | jq -r '
+ .components[]?
+ | (.path // .key) as $p
+ | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
+ | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
+ | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
+ | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
+ | select(($b | tonumber) > 0)
+ | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
+ '
+ echo ""
+ fi
+ echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
echo ""
- echo "### Komplexität"
+
+ echo "### Ratings & Issues"
echo ""
- echo "| Metrik | Wert |"
- echo "|---|---|"
- echo "| Zyklomatische Komplexität | $(val complexity) |"
- echo "| Cognitive Complexity | $(val cognitive_complexity) |"
- } >> "$GITHUB_STEP_SUMMARY"
+ echo "| Bewertung | Wert | Anzahl |"
+ echo "|---|---|---|"
+ echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
+ echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
+ echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
+ echo "| Technische Schuld | ${DEBT_FMT} | – |"
+ } >> "$SUMMARY_FILE"
+
+ - name: Fail job on Quality Gate failure
+ if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
+ run: |
+ echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
+ exit 1
From 2de83d168ed9bebaeecae33c110f38a8f1417dee Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 09:24:06 +0200
Subject: [PATCH 60/85] Test: Bash Print in CI/CD - Pipeline with detailed
information about coverage, duplication and complexity
---
.github/workflows/sonarqube.yml | 468 ++++++++++++++++----------------
1 file changed, 232 insertions(+), 236 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index a16eea5..194ea0f 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -1,243 +1,239 @@
-name: SonarCloud Analysis
-
-on:
- push:
- branches: [main, master, develop]
- pull_request:
- branches: [main, master, develop]
- workflow_dispatch:
-
-permissions:
- contents: read
-
-jobs:
- sonarcloud:
- name: Build, Test & Sonar Scan
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- # =====================================================
- # BACKEND (Python / FastAPI) – Tests + Coverage
- # =====================================================
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: "3.10"
-
- - name: Install backend dependencies
- working-directory: project/backend
- run: |
- python -m pip install --upgrade pip
- python -m pip install -r requirements.txt
- python -m pip install "pytest-cov>=5,<7" "coverage>=7"
-
- - name: Run backend tests with coverage
- working-directory: project
- run: |
- python -m pytest \
- --cov=backend \
- --cov-report=xml:backend/coverage.xml \
- --cov-report=term-missing \
- backend/tests/
- continue-on-error: true
-
- # =====================================================
- # SONARCLOUD SCAN
- # =====================================================
- - name: SonarQube Scan
- uses: SonarSource/sonarqube-scan-action@v3
- with:
- projectBaseDir: project
- args: >
- -Dsonar.projectKey=GalacticCodeGambit_LazyCook
- -Dsonar.organization=galacticcodegambit
- -Dproject.settings=sonar-project.properties
- -Dsonar.python.coverage.reportPaths=backend/coverage.xml
- -Dsonar.python.version=3.10
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
-
- - name: SonarCloud Quality Gate check
- id: sonar-qg
- uses: SonarSource/sonarqube-quality-gate-action@master
- timeout-minutes: 5
- continue-on-error: true
- with:
- scanMetadataReportFile: project/.scannerwork/report-task.txt
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
-
- # =====================================================
- # METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
- # =====================================================
- - name: Render SonarCloud metrics to Job Summary
- if: always()
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- SONAR_HOST: https://sonarcloud.io
- QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
- PR_NUMBER: ${{ github.event.pull_request.number }}
- run: |
- set +e
-
- PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
- | head -1 | cut -d'=' -f2 | tr -d ' \r\n')
-
- # PR-Kontext erkennen: Daten der PR-Analyse statt der Main-Branch holen
- PR_PARAM=""
- if [ -n "${PR_NUMBER}" ]; then
- PR_PARAM="&pullRequest=${PR_NUMBER}"
- fi
-
- METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
-
- API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}${PR_PARAM}"
- HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
- -u "${SONAR_TOKEN}:" "${API_URL}")
- RESP=$(cat /tmp/sonar.json)
-
- HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
-
- SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
-
- if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
+ name: SonarCloud Analysis
+
+ on:
+ push:
+ branches: [main, master, develop]
+ pull_request:
+ branches: [main, master, develop]
+ workflow_dispatch:
+
+ permissions:
+ contents: read
+
+ jobs:
+ sonarcloud:
+ name: Build, Test & Sonar Scan
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ # =====================================================
+ # BACKEND (Python / FastAPI) – Tests + Coverage
+ # =====================================================
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+
+ - name: Install backend dependencies
+ working-directory: project/backend
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install -r requirements.txt
+ python -m pip install "pytest-cov>=5,<7" "coverage>=7"
+
+ - name: Run backend tests with coverage
+ working-directory: project
+ run: |
+ python -m pytest \
+ --cov=backend \
+ --cov-report=xml:backend/coverage.xml \
+ --cov-report=term-missing \
+ backend/tests/
+ continue-on-error: true
+
+ # =====================================================
+ # SONARCLOUD SCAN
+ # =====================================================
+ - name: SonarQube Scan
+ uses: SonarSource/sonarqube-scan-action@v3
+ with:
+ projectBaseDir: project
+ args: >
+ -Dsonar.projectKey=GalacticCodeGambit_LazyCook
+ -Dsonar.organization=galacticcodegambit
+ -Dproject.settings=sonar-project.properties
+ -Dsonar.python.coverage.reportPaths=backend/coverage.xml
+ -Dsonar.python.version=3.10
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ - name: SonarCloud Quality Gate check
+ id: sonar-qg
+ uses: SonarSource/sonarqube-quality-gate-action@master
+ timeout-minutes: 5
+ continue-on-error: true
+ with:
+ scanMetadataReportFile: project/.scannerwork/report-task.txt
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ # =====================================================
+ # METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
+ # =====================================================
+ - name: Render SonarCloud metrics to Job Summary
+ if: always()
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_HOST: https://sonarcloud.io
+ QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ run: |
+ set +e
+
+ PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
+ | head -1 | cut -d'=' -f2 | tr -d ' \r\n')
+
+ # PR-Kontext erkennen: dann Daten der PR-Analyse holen, nicht der Main-Branch
+ PR_PARAM=""
+ if [ -n "${PR_NUMBER}" ]; then
+ PR_PARAM="&pullRequest=${PR_NUMBER}"
+ echo "PR-Modus aktiv – Daten werden für PR #${PR_NUMBER} geholt"
+ else
+ echo "Branch-Modus aktiv – Daten werden für den Default-Branch geholt"
+ fi
+
+ METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
+
+ API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}${PR_PARAM}"
+ HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
+ -u "${SONAR_TOKEN}:" "${API_URL}")
+ RESP=$(cat /tmp/sonar.json)
+
+ HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
+
+ SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
+
+ if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
+ {
+ echo "## SonarCloud Analyse"
+ echo ""
+ echo ":warning: Konnte die Metriken nicht abrufen."
+ echo ""
+ echo "- **HTTP Status:** ${HTTP_CODE}"
+ echo "- **Project Key:** ${PROJECT_KEY}"
+ echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
+ } >> "$SUMMARY_FILE"
+ exit 0
+ fi
+
+ get() {
+ echo "${RESP}" | jq -r --arg k "$1" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end'
+ }
+
+ get_sub() {
+ local sub_resp curl_args
+ curl_args=(-s -u "${SONAR_TOKEN}:" -G
+ --data-urlencode "component=${PROJECT_KEY}:$1"
+ --data-urlencode "metricKeys=$2")
+ if [ -n "${PR_NUMBER}" ]; then
+ curl_args+=(--data-urlencode "pullRequest=${PR_NUMBER}")
+ fi
+ sub_resp=$(curl "${curl_args[@]}" "${SONAR_HOST}/api/measures/component")
+ echo "${sub_resp}" | jq -r --arg k "$2" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end' 2>/dev/null || echo "-"
+ }
+
+ rating() {
+ case "$(get "$1")" in
+ 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
+ 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
+ esac
+ }
+
+ DEBT_MIN=$(get sqale_index)
+ if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
+ DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
+ DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
+ else
+ DEBT_FMT="-"
+ fi
+
+ if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
+ QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
+ else
+ QG_ICON="FAILED"; QG_EMOJI=":x:"
+ fi
+
{
- echo "## SonarCloud Analyse"
+ echo "## SonarCloud Analyse – LazyCook"
echo ""
- echo ":warning: Konnte die Metriken nicht abrufen."
+ echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
+ echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
echo ""
- echo "- **HTTP Status:** ${HTTP_CODE}"
- echo "- **Project Key:** ${PROJECT_KEY}"
- echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
- } >> "$SUMMARY_FILE"
- exit 0
- fi
-
- # Hilfsfunktion: Metrik-Wert ziehen, "-" als Default
- get() {
- echo "${RESP}" | jq -r --arg k "$1" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end'
- }
-
- # Sub-Komponente abfragen (z.B. backend, frontend/app)
- get_sub() {
- local sub_resp curl_args
- curl_args=(-s -u "${SONAR_TOKEN}:" -G
- --data-urlencode "component=${PROJECT_KEY}:$1"
- --data-urlencode "metricKeys=$2")
- if [ -n "${PR_NUMBER}" ]; then
- curl_args+=(--data-urlencode "pullRequest=${PR_NUMBER}")
- fi
- sub_resp=$(curl "${curl_args[@]}" "${SONAR_HOST}/api/measures/component")
- echo "${sub_resp}" | jq -r --arg k "$2" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end' 2>/dev/null || echo "-"
- }
-
- # Rating-Buchstaben (1.0=A ... 5.0=E)
- rating() {
- case "$(get "$1")" in
- 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
- 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
- esac
- }
-
- # Technical Debt in Stunden + Minuten formatieren
- DEBT_MIN=$(get sqale_index)
- if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
- DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
- DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
- else
- DEBT_FMT="-"
- fi
-
- # Quality-Gate-Status
- if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
- QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
- else
- QG_ICON="FAILED"; QG_EMOJI=":x:"
- fi
-
- {
- echo "## SonarCloud Analyse – LazyCook"
- echo ""
- echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
- echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
- echo ""
- echo "### Coverage"
- echo ""
- echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
- echo "|---|---|---|---|---|"
- echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
- echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
- echo ""
-
- echo "### Duplikate & Komplexität"
- echo ""
- echo "| Bereich | Metrik | Wert |"
- echo "|---|---|---|"
- echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
- echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
- echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
- echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
- echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
- echo ""
-
- echo "### Alle Dateien mit Duplikaten"
- echo ""
- } >> "$SUMMARY_FILE"
-
- # Datei-Tree mit Duplikaten holen
- TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false${PR_PARAM}"
- TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
-
- FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
-
- {
- if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
- echo "_Keine Datei hat aktuell Duplikate._"
+ echo "### Coverage"
echo ""
- else
- echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
+ echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
+ echo "|---|---|---|---|---|"
+ echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
+ echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
echo ""
- echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
- echo "|---|---:|---:|---:|---:|"
- echo "${TREE_RESP}" | jq -r '
- .components[]?
- | (.path // .key) as $p
- | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
- | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
- | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
- | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
- | select(($b | tonumber) > 0)
- | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
- '
+
+ echo "### Duplikate & Komplexität"
echo ""
- fi
- echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
- echo ""
-
- echo "### Ratings & Issues"
- echo ""
- echo "| Bewertung | Wert | Anzahl |"
- echo "|---|---|---|"
- echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
- echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
- echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
- echo "| Technische Schuld | ${DEBT_FMT} | – |"
- } >> "$SUMMARY_FILE"
+ echo "| Bereich | Metrik | Wert |"
+ echo "|---|---|---|"
+ echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
+ echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
+ echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
+ echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
+ echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
+ echo ""
+
+ echo "### Alle Dateien mit Duplikaten"
+ echo ""
+ } >> "$SUMMARY_FILE"
+
+ TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false${PR_PARAM}"
+ TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
+
+ FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
+
+ {
+ if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
+ echo "_Keine Datei hat aktuell Duplikate._"
+ echo ""
+ else
+ echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
+ echo ""
+ echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
+ echo "|---|---:|---:|---:|---:|"
+ echo "${TREE_RESP}" | jq -r '
+ .components[]?
+ | (.path // .key) as $p
+ | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
+ | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
+ | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
+ | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
+ | select(($b | tonumber) > 0)
+ | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
+ '
+ echo ""
+ fi
+ echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
+ echo ""
+
+ echo "### Ratings & Issues"
+ echo ""
+ echo "| Bewertung | Wert | Anzahl |"
+ echo "|---|---|---|"
+ echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
+ echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
+ echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
+ echo "| Technische Schuld | ${DEBT_FMT} | – |"
+ } >> "$SUMMARY_FILE"
- - name: Fail job on Quality Gate failure
- if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
- run: |
- echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
- exit 1
+ - name: Fail job on Quality Gate failure
+ if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
+ run: |
+ echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
\ No newline at end of file
From c07e86914277f2ff178c92ee7b2605ba4e340f00 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 09:26:49 +0200
Subject: [PATCH 61/85] Test: Bash Print in CI/CD - Pipeline with detailed
information about coverage, duplication and complexity
---
.github/workflows/sonarqube.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 194ea0f..0bc72f6 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -44,7 +44,7 @@
--cov-report=xml:backend/coverage.xml \
--cov-report=term-missing \
backend/tests/
- continue-on-error: true
+ continue-on-error: true
# =====================================================
# SONARCLOUD SCAN
From 6bb6a65820e498db40d4e21c3cca3d2d5afb08d0 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 09:32:08 +0200
Subject: [PATCH 62/85] Test: Bash Print in CI/CD - Pipeline with detailed
information about coverage, duplication and complexity
---
.github/workflows/sonarqube.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 0bc72f6..c3bfb59 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -236,4 +236,5 @@
- name: Fail job on Quality Gate failure
if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
run: |
- echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
\ No newline at end of file
+ echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
+ exit 1
\ No newline at end of file
From 119f4acd7f2bc5359b84de7509d1a38b60d0dabf Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 09:50:04 +0200
Subject: [PATCH 63/85] Revert "Apply suggestions from code review"
This reverts commit 0e2bc05b
---
.github/workflows/sonarqube.yml | 463 ++++++++++++++++----------------
docs/Review-Protokoll-01.md | 2 +-
2 files changed, 231 insertions(+), 234 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index c3bfb59..bf6704a 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -1,240 +1,237 @@
- name: SonarCloud Analysis
-
- on:
- push:
- branches: [main, master, develop]
- pull_request:
- branches: [main, master, develop]
- workflow_dispatch:
-
- permissions:
- contents: read
-
- jobs:
- sonarcloud:
- name: Build, Test & Sonar Scan
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- # =====================================================
- # BACKEND (Python / FastAPI) – Tests + Coverage
- # =====================================================
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: "3.10"
-
- - name: Install backend dependencies
- working-directory: project/backend
- run: |
- python -m pip install --upgrade pip
- python -m pip install -r requirements.txt
- python -m pip install "pytest-cov>=5,<7" "coverage>=7"
-
- - name: Run backend tests with coverage
- working-directory: project
- run: |
- python -m pytest \
- --cov=backend \
- --cov-report=xml:backend/coverage.xml \
- --cov-report=term-missing \
- backend/tests/
- continue-on-error: true
-
- # =====================================================
- # SONARCLOUD SCAN
- # =====================================================
- - name: SonarQube Scan
- uses: SonarSource/sonarqube-scan-action@v3
- with:
- projectBaseDir: project
- args: >
- -Dsonar.projectKey=GalacticCodeGambit_LazyCook
- -Dsonar.organization=galacticcodegambit
- -Dproject.settings=sonar-project.properties
- -Dsonar.python.coverage.reportPaths=backend/coverage.xml
- -Dsonar.python.version=3.10
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
-
- - name: SonarCloud Quality Gate check
- id: sonar-qg
- uses: SonarSource/sonarqube-quality-gate-action@master
- timeout-minutes: 5
- continue-on-error: true
- with:
- scanMetadataReportFile: project/.scannerwork/report-task.txt
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
-
- # =====================================================
- # METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
- # =====================================================
- - name: Render SonarCloud metrics to Job Summary
- if: always()
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- SONAR_HOST: https://sonarcloud.io
- QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
- PR_NUMBER: ${{ github.event.pull_request.number }}
- run: |
- set +e
-
- PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
- | head -1 | cut -d'=' -f2 | tr -d ' \r\n')
-
- # PR-Kontext erkennen: dann Daten der PR-Analyse holen, nicht der Main-Branch
- PR_PARAM=""
- if [ -n "${PR_NUMBER}" ]; then
- PR_PARAM="&pullRequest=${PR_NUMBER}"
- echo "PR-Modus aktiv – Daten werden für PR #${PR_NUMBER} geholt"
- else
- echo "Branch-Modus aktiv – Daten werden für den Default-Branch geholt"
- fi
-
- METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
-
- API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}${PR_PARAM}"
- HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
- -u "${SONAR_TOKEN}:" "${API_URL}")
- RESP=$(cat /tmp/sonar.json)
-
- HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
-
- SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
-
- if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
- {
- echo "## SonarCloud Analyse"
- echo ""
- echo ":warning: Konnte die Metriken nicht abrufen."
- echo ""
- echo "- **HTTP Status:** ${HTTP_CODE}"
- echo "- **Project Key:** ${PROJECT_KEY}"
- echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
- } >> "$SUMMARY_FILE"
- exit 0
- fi
-
- get() {
- echo "${RESP}" | jq -r --arg k "$1" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end'
- }
-
- get_sub() {
- local sub_resp curl_args
- curl_args=(-s -u "${SONAR_TOKEN}:" -G
- --data-urlencode "component=${PROJECT_KEY}:$1"
- --data-urlencode "metricKeys=$2")
- if [ -n "${PR_NUMBER}" ]; then
- curl_args+=(--data-urlencode "pullRequest=${PR_NUMBER}")
- fi
- sub_resp=$(curl "${curl_args[@]}" "${SONAR_HOST}/api/measures/component")
- echo "${sub_resp}" | jq -r --arg k "$2" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end' 2>/dev/null || echo "-"
- }
-
- rating() {
- case "$(get "$1")" in
- 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
- 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
- esac
- }
-
- DEBT_MIN=$(get sqale_index)
- if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
- DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
- DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
- else
- DEBT_FMT="-"
- fi
-
- if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
- QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
- else
- QG_ICON="FAILED"; QG_EMOJI=":x:"
- fi
-
+name: SonarCloud Analysis
+
+on:
+ push:
+ branches: [main, master, develop]
+ pull_request:
+ branches: [main, master, develop]
+ workflow_dispatch:
+
+jobs:
+ sonarcloud:
+ name: Build, Test & Sonar Scan
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ # =====================================================
+ # BACKEND (Python / FastAPI) – Tests + Coverage
+ # =====================================================
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+
+ - name: Install backend dependencies
+ working-directory: project/backend
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install -r requirements.txt
+ python -m pip install "pytest-cov>=5,<7" "coverage>=7"
+
+ - name: Run backend tests with coverage
+ working-directory: project
+ run: |
+ python -m pytest \
+ --cov=backend \
+ --cov-report=xml:backend/coverage.xml \
+ --cov-report=term-missing \
+ backend/tests/
+ continue-on-error: true
+
+ # =====================================================
+ # SONARCLOUD SCAN
+ # =====================================================
+ - name: SonarQube Scan
+ uses: SonarSource/sonarqube-scan-action@v3
+ with:
+ projectBaseDir: project
+ args: >
+ -Dsonar.projectKey=GalacticCodeGambit_LazyCook
+ -Dsonar.organization=galacticcodegambit
+ -Dproject.settings=sonar-project.properties
+ -Dsonar.python.coverage.reportPaths=backend/coverage.xml
+ -Dsonar.python.version=3.10
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ - name: SonarCloud Quality Gate check
+ id: sonar-qg
+ uses: SonarSource/sonarqube-quality-gate-action@master
+ timeout-minutes: 5
+ continue-on-error: true
+ with:
+ scanMetadataReportFile: project/.scannerwork/report-task.txt
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ # =====================================================
+ # METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
+ # =====================================================
+ - name: Render SonarCloud metrics to Job Summary
+ if: always()
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_HOST: https://sonarcloud.io
+ QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ run: |
+ set +e
+
+ PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
+ | head -1 | cut -d'=' -f2 | tr -d ' \r\n')
+
+ # PR-Kontext erkennen: dann Daten der PR-Analyse holen, nicht der Main-Branch
+ PR_PARAM=""
+ if [ -n "${PR_NUMBER}" ]; then
+ PR_PARAM="&pullRequest=${PR_NUMBER}"
+ echo "PR-Modus aktiv – Daten werden für PR #${PR_NUMBER} geholt"
+ else
+ echo "Branch-Modus aktiv – Daten werden für den Default-Branch geholt"
+ fi
+
+ METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
+
+ API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}${PR_PARAM}"
+ HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
+ -u "${SONAR_TOKEN}:" "${API_URL}")
+ RESP=$(cat /tmp/sonar.json)
+
+ HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
+
+ SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
+
+ if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
{
- echo "## SonarCloud Analyse – LazyCook"
+ echo "## SonarCloud Analyse"
echo ""
- echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
- echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
- echo ""
- echo "### Coverage"
- echo ""
- echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
- echo "|---|---|---|---|---|"
- echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
- echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
- echo ""
-
- echo "### Duplikate & Komplexität"
- echo ""
- echo "| Bereich | Metrik | Wert |"
- echo "|---|---|---|"
- echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
- echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
- echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
- echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
- echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
- echo ""
-
- echo "### Alle Dateien mit Duplikaten"
+ echo ":warning: Konnte die Metriken nicht abrufen."
echo ""
+ echo "- **HTTP Status:** ${HTTP_CODE}"
+ echo "- **Project Key:** ${PROJECT_KEY}"
+ echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
} >> "$SUMMARY_FILE"
-
- TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false${PR_PARAM}"
- TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
-
- FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
-
- {
- if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
- echo "_Keine Datei hat aktuell Duplikate._"
- echo ""
- else
- echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
- echo ""
- echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
- echo "|---|---:|---:|---:|---:|"
- echo "${TREE_RESP}" | jq -r '
- .components[]?
- | (.path // .key) as $p
- | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
- | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
- | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
- | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
- | select(($b | tonumber) > 0)
- | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
- '
- echo ""
- fi
- echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
+ exit 0
+ fi
+
+ get() {
+ echo "${RESP}" | jq -r --arg k "$1" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end'
+ }
+
+ get_sub() {
+ local sub_resp curl_args
+ curl_args=(-s -u "${SONAR_TOKEN}:" -G
+ --data-urlencode "component=${PROJECT_KEY}:$1"
+ --data-urlencode "metricKeys=$2")
+ if [ -n "${PR_NUMBER}" ]; then
+ curl_args+=(--data-urlencode "pullRequest=${PR_NUMBER}")
+ fi
+ sub_resp=$(curl "${curl_args[@]}" "${SONAR_HOST}/api/measures/component")
+ echo "${sub_resp}" | jq -r --arg k "$2" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end' 2>/dev/null || echo "-"
+ }
+
+ rating() {
+ case "$(get "$1")" in
+ 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
+ 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
+ esac
+ }
+
+ DEBT_MIN=$(get sqale_index)
+ if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
+ DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
+ DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
+ else
+ DEBT_FMT="-"
+ fi
+
+ if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
+ QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
+ else
+ QG_ICON="FAILED"; QG_EMOJI=":x:"
+ fi
+
+ {
+ echo "## SonarCloud Analyse – LazyCook"
+ echo ""
+ echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
+ echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
+ echo ""
+ echo "### Coverage"
+ echo ""
+ echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
+ echo "|---|---|---|---|---|"
+ echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
+ echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
+ echo ""
+
+ echo "### Duplikate & Komplexität"
+ echo ""
+ echo "| Bereich | Metrik | Wert |"
+ echo "|---|---|---|"
+ echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
+ echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
+ echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
+ echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
+ echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
+ echo ""
+
+ echo "### Alle Dateien mit Duplikaten"
+ echo ""
+ } >> "$SUMMARY_FILE"
+
+ TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false${PR_PARAM}"
+ TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
+
+ FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
+
+ {
+ if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
+ echo "_Keine Datei hat aktuell Duplikate._"
echo ""
-
- echo "### Ratings & Issues"
+ else
+ echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
echo ""
- echo "| Bewertung | Wert | Anzahl |"
- echo "|---|---|---|"
- echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
- echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
- echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
- echo "| Technische Schuld | ${DEBT_FMT} | – |"
- } >> "$SUMMARY_FILE"
+ echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
+ echo "|---|---:|---:|---:|---:|"
+ echo "${TREE_RESP}" | jq -r '
+ .components[]?
+ | (.path // .key) as $p
+ | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
+ | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
+ | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
+ | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
+ | select(($b | tonumber) > 0)
+ | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
+ '
+ echo ""
+ fi
+ echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
+ echo ""
+
+ echo "### Ratings & Issues"
+ echo ""
+ echo "| Bewertung | Wert | Anzahl |"
+ echo "|---|---|---|"
+ echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
+ echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
+ echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
+ echo "| Technische Schuld | ${DEBT_FMT} | – |"
+ } >> "$SUMMARY_FILE"
- - name: Fail job on Quality Gate failure
- if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
- run: |
- echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
- exit 1
\ No newline at end of file
+ - name: Fail job on Quality Gate failure
+ if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
+ run: |
+ echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
+ exit 1
\ No newline at end of file
diff --git a/docs/Review-Protokoll-01.md b/docs/Review-Protokoll-01.md
index d49bd59..5ce09da 100644
--- a/docs/Review-Protokoll-01.md
+++ b/docs/Review-Protokoll-01.md
@@ -1,6 +1,6 @@
# Review-Protokoll-01
### **Datum** (Startzeit und Endzeit):
-12.05.2026 16:00 bis 16:20
+12.05.2026 16.:00 bis 16:20
### **Teilnehmende**:
- Moderator: PrussianBaron
From f38c1c81228408d1fbb7ec57ea28ad7aa19c9d6e Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 09:54:29 +0200
Subject: [PATCH 64/85] Revert "Test: Sonarqube workflow with result print in
CI-Pipeline"
This reverts commit c7647ced34c2d0d37108e44425184fb420d560d5.
---
.github/workflows/sonarqube.yml | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index bf6704a..932cf02 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -104,6 +104,17 @@ jobs:
HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
+ # DIAGNOSE – wir wollen sehen, was der Script konkret sieht
+ echo "===== DEBUG ====="
+ echo "PROJECT_KEY = '${PROJECT_KEY}'"
+ echo "API_URL = ${API_URL}"
+ echo "HTTP_CODE = ${HTTP_CODE}"
+ echo "HAS_MEASURES= ${HAS_MEASURES}"
+ echo "RESP first 800 chars:"
+ echo "${RESP}" | head -c 800
+ echo ""
+ echo "===== /DEBUG ====="
+
SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
@@ -168,6 +179,17 @@ jobs:
echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
echo ""
+ echo "Debug-Info (kann später raus)
"
+ echo ""
+ echo "- PROJECT_KEY: ${PROJECT_KEY}"
+ echo "- HTTP_CODE: ${HTTP_CODE}"
+ echo "- HAS_MEASURES: ${HAS_MEASURES}"
+ echo "- coverage value direkt aus get: '$(get coverage)'"
+ echo "- ncloc direkt aus get: '$(get ncloc)'"
+ echo "- duplicated_lines_density direkt aus get: '$(get duplicated_lines_density)'"
+ echo ""
+ echo " "
+ echo ""
echo "### Coverage"
echo ""
echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
From 0a4051d3ffbdc56574d0ad8e7eacaa8b66679bc2 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 10:11:59 +0200
Subject: [PATCH 65/85] Revert "Revert "Apply suggestions from code review""
This reverts commit 119f4acd
---
.github/workflows/sonarqube.yml | 485 +++++++++++++++-----------------
docs/Review-Protokoll-01.md | 2 +-
2 files changed, 234 insertions(+), 253 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 932cf02..c3bfb59 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -1,259 +1,240 @@
-name: SonarCloud Analysis
-
-on:
- push:
- branches: [main, master, develop]
- pull_request:
- branches: [main, master, develop]
- workflow_dispatch:
-
-jobs:
- sonarcloud:
- name: Build, Test & Sonar Scan
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- # =====================================================
- # BACKEND (Python / FastAPI) – Tests + Coverage
- # =====================================================
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: "3.10"
-
- - name: Install backend dependencies
- working-directory: project/backend
- run: |
- python -m pip install --upgrade pip
- python -m pip install -r requirements.txt
- python -m pip install "pytest-cov>=5,<7" "coverage>=7"
-
- - name: Run backend tests with coverage
- working-directory: project
- run: |
- python -m pytest \
- --cov=backend \
- --cov-report=xml:backend/coverage.xml \
- --cov-report=term-missing \
- backend/tests/
- continue-on-error: true
-
- # =====================================================
- # SONARCLOUD SCAN
- # =====================================================
- - name: SonarQube Scan
- uses: SonarSource/sonarqube-scan-action@v3
- with:
- projectBaseDir: project
- args: >
- -Dsonar.projectKey=GalacticCodeGambit_LazyCook
- -Dsonar.organization=galacticcodegambit
- -Dproject.settings=sonar-project.properties
- -Dsonar.python.coverage.reportPaths=backend/coverage.xml
- -Dsonar.python.version=3.10
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
-
- - name: SonarCloud Quality Gate check
- id: sonar-qg
- uses: SonarSource/sonarqube-quality-gate-action@master
- timeout-minutes: 5
- continue-on-error: true
- with:
- scanMetadataReportFile: project/.scannerwork/report-task.txt
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
-
- # =====================================================
- # METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
- # =====================================================
- - name: Render SonarCloud metrics to Job Summary
- if: always()
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- SONAR_HOST: https://sonarcloud.io
- QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
- PR_NUMBER: ${{ github.event.pull_request.number }}
- run: |
- set +e
-
- PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
- | head -1 | cut -d'=' -f2 | tr -d ' \r\n')
-
- # PR-Kontext erkennen: dann Daten der PR-Analyse holen, nicht der Main-Branch
- PR_PARAM=""
- if [ -n "${PR_NUMBER}" ]; then
- PR_PARAM="&pullRequest=${PR_NUMBER}"
- echo "PR-Modus aktiv – Daten werden für PR #${PR_NUMBER} geholt"
- else
- echo "Branch-Modus aktiv – Daten werden für den Default-Branch geholt"
- fi
-
- METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
-
- API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}${PR_PARAM}"
- HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
- -u "${SONAR_TOKEN}:" "${API_URL}")
- RESP=$(cat /tmp/sonar.json)
-
- HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
-
- # DIAGNOSE – wir wollen sehen, was der Script konkret sieht
- echo "===== DEBUG ====="
- echo "PROJECT_KEY = '${PROJECT_KEY}'"
- echo "API_URL = ${API_URL}"
- echo "HTTP_CODE = ${HTTP_CODE}"
- echo "HAS_MEASURES= ${HAS_MEASURES}"
- echo "RESP first 800 chars:"
- echo "${RESP}" | head -c 800
- echo ""
- echo "===== /DEBUG ====="
-
- SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
-
- if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
+ name: SonarCloud Analysis
+
+ on:
+ push:
+ branches: [main, master, develop]
+ pull_request:
+ branches: [main, master, develop]
+ workflow_dispatch:
+
+ permissions:
+ contents: read
+
+ jobs:
+ sonarcloud:
+ name: Build, Test & Sonar Scan
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ # =====================================================
+ # BACKEND (Python / FastAPI) – Tests + Coverage
+ # =====================================================
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+
+ - name: Install backend dependencies
+ working-directory: project/backend
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install -r requirements.txt
+ python -m pip install "pytest-cov>=5,<7" "coverage>=7"
+
+ - name: Run backend tests with coverage
+ working-directory: project
+ run: |
+ python -m pytest \
+ --cov=backend \
+ --cov-report=xml:backend/coverage.xml \
+ --cov-report=term-missing \
+ backend/tests/
+ continue-on-error: true
+
+ # =====================================================
+ # SONARCLOUD SCAN
+ # =====================================================
+ - name: SonarQube Scan
+ uses: SonarSource/sonarqube-scan-action@v3
+ with:
+ projectBaseDir: project
+ args: >
+ -Dsonar.projectKey=GalacticCodeGambit_LazyCook
+ -Dsonar.organization=galacticcodegambit
+ -Dproject.settings=sonar-project.properties
+ -Dsonar.python.coverage.reportPaths=backend/coverage.xml
+ -Dsonar.python.version=3.10
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ - name: SonarCloud Quality Gate check
+ id: sonar-qg
+ uses: SonarSource/sonarqube-quality-gate-action@master
+ timeout-minutes: 5
+ continue-on-error: true
+ with:
+ scanMetadataReportFile: project/.scannerwork/report-task.txt
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ # =====================================================
+ # METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
+ # =====================================================
+ - name: Render SonarCloud metrics to Job Summary
+ if: always()
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_HOST: https://sonarcloud.io
+ QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ run: |
+ set +e
+
+ PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
+ | head -1 | cut -d'=' -f2 | tr -d ' \r\n')
+
+ # PR-Kontext erkennen: dann Daten der PR-Analyse holen, nicht der Main-Branch
+ PR_PARAM=""
+ if [ -n "${PR_NUMBER}" ]; then
+ PR_PARAM="&pullRequest=${PR_NUMBER}"
+ echo "PR-Modus aktiv – Daten werden für PR #${PR_NUMBER} geholt"
+ else
+ echo "Branch-Modus aktiv – Daten werden für den Default-Branch geholt"
+ fi
+
+ METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
+
+ API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}${PR_PARAM}"
+ HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
+ -u "${SONAR_TOKEN}:" "${API_URL}")
+ RESP=$(cat /tmp/sonar.json)
+
+ HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
+
+ SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
+
+ if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
+ {
+ echo "## SonarCloud Analyse"
+ echo ""
+ echo ":warning: Konnte die Metriken nicht abrufen."
+ echo ""
+ echo "- **HTTP Status:** ${HTTP_CODE}"
+ echo "- **Project Key:** ${PROJECT_KEY}"
+ echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
+ } >> "$SUMMARY_FILE"
+ exit 0
+ fi
+
+ get() {
+ echo "${RESP}" | jq -r --arg k "$1" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end'
+ }
+
+ get_sub() {
+ local sub_resp curl_args
+ curl_args=(-s -u "${SONAR_TOKEN}:" -G
+ --data-urlencode "component=${PROJECT_KEY}:$1"
+ --data-urlencode "metricKeys=$2")
+ if [ -n "${PR_NUMBER}" ]; then
+ curl_args+=(--data-urlencode "pullRequest=${PR_NUMBER}")
+ fi
+ sub_resp=$(curl "${curl_args[@]}" "${SONAR_HOST}/api/measures/component")
+ echo "${sub_resp}" | jq -r --arg k "$2" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end' 2>/dev/null || echo "-"
+ }
+
+ rating() {
+ case "$(get "$1")" in
+ 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
+ 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
+ esac
+ }
+
+ DEBT_MIN=$(get sqale_index)
+ if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
+ DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
+ DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
+ else
+ DEBT_FMT="-"
+ fi
+
+ if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
+ QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
+ else
+ QG_ICON="FAILED"; QG_EMOJI=":x:"
+ fi
+
{
- echo "## SonarCloud Analyse"
+ echo "## SonarCloud Analyse – LazyCook"
echo ""
- echo ":warning: Konnte die Metriken nicht abrufen."
+ echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
+ echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
echo ""
- echo "- **HTTP Status:** ${HTTP_CODE}"
- echo "- **Project Key:** ${PROJECT_KEY}"
- echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
- } >> "$SUMMARY_FILE"
- exit 0
- fi
-
- get() {
- echo "${RESP}" | jq -r --arg k "$1" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end'
- }
-
- get_sub() {
- local sub_resp curl_args
- curl_args=(-s -u "${SONAR_TOKEN}:" -G
- --data-urlencode "component=${PROJECT_KEY}:$1"
- --data-urlencode "metricKeys=$2")
- if [ -n "${PR_NUMBER}" ]; then
- curl_args+=(--data-urlencode "pullRequest=${PR_NUMBER}")
- fi
- sub_resp=$(curl "${curl_args[@]}" "${SONAR_HOST}/api/measures/component")
- echo "${sub_resp}" | jq -r --arg k "$2" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end' 2>/dev/null || echo "-"
- }
-
- rating() {
- case "$(get "$1")" in
- 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
- 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
- esac
- }
-
- DEBT_MIN=$(get sqale_index)
- if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
- DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
- DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
- else
- DEBT_FMT="-"
- fi
-
- if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
- QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
- else
- QG_ICON="FAILED"; QG_EMOJI=":x:"
- fi
-
- {
- echo "## SonarCloud Analyse – LazyCook"
- echo ""
- echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
- echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
- echo ""
- echo "Debug-Info (kann später raus)
"
- echo ""
- echo "- PROJECT_KEY: ${PROJECT_KEY}"
- echo "- HTTP_CODE: ${HTTP_CODE}"
- echo "- HAS_MEASURES: ${HAS_MEASURES}"
- echo "- coverage value direkt aus get: '$(get coverage)'"
- echo "- ncloc direkt aus get: '$(get ncloc)'"
- echo "- duplicated_lines_density direkt aus get: '$(get duplicated_lines_density)'"
- echo ""
- echo " "
- echo ""
- echo "### Coverage"
- echo ""
- echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
- echo "|---|---|---|---|---|"
- echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
- echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
- echo ""
-
- echo "### Duplikate & Komplexität"
- echo ""
- echo "| Bereich | Metrik | Wert |"
- echo "|---|---|---|"
- echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
- echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
- echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
- echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
- echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
- echo ""
-
- echo "### Alle Dateien mit Duplikaten"
- echo ""
- } >> "$SUMMARY_FILE"
-
- TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false${PR_PARAM}"
- TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
-
- FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
-
- {
- if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
- echo "_Keine Datei hat aktuell Duplikate._"
+ echo "### Coverage"
echo ""
- else
- echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
+ echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
+ echo "|---|---|---|---|---|"
+ echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
+ echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
echo ""
- echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
- echo "|---|---:|---:|---:|---:|"
- echo "${TREE_RESP}" | jq -r '
- .components[]?
- | (.path // .key) as $p
- | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
- | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
- | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
- | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
- | select(($b | tonumber) > 0)
- | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
- '
+
+ echo "### Duplikate & Komplexität"
echo ""
- fi
- echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
- echo ""
-
- echo "### Ratings & Issues"
- echo ""
- echo "| Bewertung | Wert | Anzahl |"
- echo "|---|---|---|"
- echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
- echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
- echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
- echo "| Technische Schuld | ${DEBT_FMT} | – |"
- } >> "$SUMMARY_FILE"
+ echo "| Bereich | Metrik | Wert |"
+ echo "|---|---|---|"
+ echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
+ echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
+ echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
+ echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
+ echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
+ echo ""
+
+ echo "### Alle Dateien mit Duplikaten"
+ echo ""
+ } >> "$SUMMARY_FILE"
+
+ TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false${PR_PARAM}"
+ TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
+
+ FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
+
+ {
+ if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
+ echo "_Keine Datei hat aktuell Duplikate._"
+ echo ""
+ else
+ echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
+ echo ""
+ echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
+ echo "|---|---:|---:|---:|---:|"
+ echo "${TREE_RESP}" | jq -r '
+ .components[]?
+ | (.path // .key) as $p
+ | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
+ | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
+ | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
+ | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
+ | select(($b | tonumber) > 0)
+ | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
+ '
+ echo ""
+ fi
+ echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
+ echo ""
+
+ echo "### Ratings & Issues"
+ echo ""
+ echo "| Bewertung | Wert | Anzahl |"
+ echo "|---|---|---|"
+ echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
+ echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
+ echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
+ echo "| Technische Schuld | ${DEBT_FMT} | – |"
+ } >> "$SUMMARY_FILE"
- - name: Fail job on Quality Gate failure
- if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
- run: |
- echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
- exit 1
\ No newline at end of file
+ - name: Fail job on Quality Gate failure
+ if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
+ run: |
+ echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
+ exit 1
\ No newline at end of file
diff --git a/docs/Review-Protokoll-01.md b/docs/Review-Protokoll-01.md
index 5ce09da..d49bd59 100644
--- a/docs/Review-Protokoll-01.md
+++ b/docs/Review-Protokoll-01.md
@@ -1,6 +1,6 @@
# Review-Protokoll-01
### **Datum** (Startzeit und Endzeit):
-12.05.2026 16.:00 bis 16:20
+12.05.2026 16:00 bis 16:20
### **Teilnehmende**:
- Moderator: PrussianBaron
From 77bc4c41c825af7235121f71eaf500b640241990 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 10:19:45 +0200
Subject: [PATCH 66/85] Test: Bash Print in CI/CD - Pipeline with detailed
information about coverage, duplication and complexity
---
.github/workflows/sonarqube.yml | 50 ++++++++++++++++++++++++++-------
1 file changed, 40 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index c3bfb59..45f9763 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -104,8 +104,14 @@
HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
-u "${SONAR_TOKEN}:" "${API_URL}")
RESP=$(cat /tmp/sonar.json)
-
+
HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
+
+ # Zusätzlich Branch-Daten holen für Metriken, die im PR-Modus leer sind
+ # (z.B. ncloc, complexity, cognitive_complexity – Projekt-strukturelle Werte)
+ BRANCH_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}"
+ curl -s -o /tmp/sonar_branch.json -u "${SONAR_TOKEN}:" "${BRANCH_URL}" >/dev/null
+ BRANCH_RESP=$(cat /tmp/sonar_branch.json)
SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
@@ -122,23 +128,47 @@
exit 0
fi
+ # Holt Metrik primär aus PR-Response, fällt auf Branch-Response zurück
get() {
- echo "${RESP}" | jq -r --arg k "$1" \
+ local v
+ v=$(echo "${RESP}" | jq -r --arg k "$1" \
'if (.component? and .component.measures?) then
((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end'
+ else "-" end')
+ if [ "$v" = "-" ] || [ -z "$v" ]; then
+ v=$(echo "${BRANCH_RESP}" | jq -r --arg k "$1" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end')
+ fi
+ echo "$v"
}
+ # Holt Metrik für Sub-Komponente. Versucht erst PR-Scope, fällt auf Branch zurück.
get_sub() {
- local sub_resp curl_args
- curl_args=(-s -u "${SONAR_TOKEN}:" -G
- --data-urlencode "component=${PROJECT_KEY}:$1"
- --data-urlencode "metricKeys=$2")
+ local v sub_resp_pr sub_resp_branch
+ # PR-Versuch (falls PR-Kontext)
if [ -n "${PR_NUMBER}" ]; then
- curl_args+=(--data-urlencode "pullRequest=${PR_NUMBER}")
+ sub_resp_pr=$(curl -s -u "${SONAR_TOKEN}:" -G \
+ --data-urlencode "component=${PROJECT_KEY}:$1" \
+ --data-urlencode "metricKeys=$2" \
+ --data-urlencode "pullRequest=${PR_NUMBER}" \
+ "${SONAR_HOST}/api/measures/component")
+ v=$(echo "${sub_resp_pr}" | jq -r --arg k "$2" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end' 2>/dev/null)
+ if [ "$v" != "-" ] && [ -n "$v" ]; then
+ echo "$v"
+ return
+ fi
fi
- sub_resp=$(curl "${curl_args[@]}" "${SONAR_HOST}/api/measures/component")
- echo "${sub_resp}" | jq -r --arg k "$2" \
+ # Fallback: Branch-/Default-Query ohne PR
+ sub_resp_branch=$(curl -s -u "${SONAR_TOKEN}:" -G \
+ --data-urlencode "component=${PROJECT_KEY}:$1" \
+ --data-urlencode "metricKeys=$2" \
+ "${SONAR_HOST}/api/measures/component")
+ echo "${sub_resp_branch}" | jq -r --arg k "$2" \
'if (.component? and .component.measures?) then
((.component.measures[] | select(.metric==$k) | .value) // "-")
else "-" end' 2>/dev/null || echo "-"
From 847fb33f823b0356cfbf855356dd7bc196c4e19f Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 10:24:21 +0200
Subject: [PATCH 67/85] Test: Bash Print in CI/CD - Pipeline with detailed
information about coverage, duplication and complexity
---
.github/workflows/sonarqube.yml | 48 ++++++++++++++++-----------------
1 file changed, 24 insertions(+), 24 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 45f9763..e3a0155 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -144,34 +144,34 @@
echo "$v"
}
- # Holt Metrik für Sub-Komponente. Versucht erst PR-Scope, fällt auf Branch zurück.
+ # Holt Metrik für Sub-Komponente.
+ # SonarCloud-Sub-Komponente kann ':' nicht immer per
+ # measures/component-Endpoint adressieren – wir nutzen component_tree
+ # mit dem 'q'-Filter, der nach Pfad-Suffix sucht.
get_sub() {
- local v sub_resp_pr sub_resp_branch
- # PR-Versuch (falls PR-Kontext)
- if [ -n "${PR_NUMBER}" ]; then
- sub_resp_pr=$(curl -s -u "${SONAR_TOKEN}:" -G \
- --data-urlencode "component=${PROJECT_KEY}:$1" \
- --data-urlencode "metricKeys=$2" \
- --data-urlencode "pullRequest=${PR_NUMBER}" \
- "${SONAR_HOST}/api/measures/component")
- v=$(echo "${sub_resp_pr}" | jq -r --arg k "$2" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end' 2>/dev/null)
- if [ "$v" != "-" ] && [ -n "$v" ]; then
+ local path="$1"
+ local metric="$2"
+ local tree_url tree_resp v
+
+ # Branch-Tree erstmal, danach PR falls vorhanden – beide pruefen
+ for try_pr in branch pr; do
+ tree_url="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=${metric}&qualifiers=DIR&q=${path}&ps=20"
+ if [ "$try_pr" = "pr" ] && [ -n "${PR_NUMBER}" ]; then
+ tree_url="${tree_url}&pullRequest=${PR_NUMBER}"
+ elif [ "$try_pr" = "pr" ]; then
+ continue
+ fi
+ tree_resp=$(curl -s -u "${SONAR_TOKEN}:" "${tree_url}")
+ # Sucht in den Components die mit path enden ODER deren path == argument
+ v=$(echo "${tree_resp}" | jq -r --arg p "$path" --arg k "$metric" \
+ '[.components[]? | select(.path == $p)]
+ | .[0].measures[]? | select(.metric==$k) | .value' 2>/dev/null)
+ if [ -n "$v" ] && [ "$v" != "null" ] && [ "$v" != "-" ]; then
echo "$v"
return
fi
- fi
- # Fallback: Branch-/Default-Query ohne PR
- sub_resp_branch=$(curl -s -u "${SONAR_TOKEN}:" -G \
- --data-urlencode "component=${PROJECT_KEY}:$1" \
- --data-urlencode "metricKeys=$2" \
- "${SONAR_HOST}/api/measures/component")
- echo "${sub_resp_branch}" | jq -r --arg k "$2" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end' 2>/dev/null || echo "-"
+ done
+ echo "-"
}
rating() {
From 2993dc3cb40e3ffa3d05f982d65c928b98013653 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 10:40:43 +0200
Subject: [PATCH 68/85] Test: Bash Print in CI/CD - Pipeline with detailed
information about coverage, duplication and complexity
---
.github/workflows/sonarqube-main.yml | 275 +++++++++++++++++++++++++++
.github/workflows/sonarqube.yml | 4 +-
2 files changed, 277 insertions(+), 2 deletions(-)
create mode 100644 .github/workflows/sonarqube-main.yml
diff --git a/.github/workflows/sonarqube-main.yml b/.github/workflows/sonarqube-main.yml
new file mode 100644
index 0000000..81dfb69
--- /dev/null
+++ b/.github/workflows/sonarqube-main.yml
@@ -0,0 +1,275 @@
+name: SonarCloud Full Metrics (main branch)
+
+on:
+ push:
+ branches: [main, master, sonarqube]
+ pull_request:
+ branches: [main, master, sonarqube]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ full-metrics:
+ name: Full Project Report
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ # =====================================================
+ # Backend-Tests + Coverage
+ # =====================================================
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+
+ - name: Install backend deps
+ working-directory: project/backend
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install -r requirements.txt
+ python -m pip install "pytest-cov>=5,<7" "coverage>=7"
+
+ - name: Run backend tests with coverage
+ working-directory: project
+ run: |
+ python -m pytest \
+ --cov=backend \
+ --cov-report=xml:backend/coverage.xml \
+ backend/tests/
+ continue-on-error: true
+
+ # =====================================================
+ # SonarCloud Scan
+ # =====================================================
+ - name: SonarQube Scan
+ uses: SonarSource/sonarqube-scan-action@v3
+ with:
+ projectBaseDir: project
+ args: >
+ -Dsonar.projectKey=GalacticCodeGambit_LazyCook
+ -Dsonar.organization=galacticcodegambit
+ -Dproject.settings=sonar-project.properties
+ -Dsonar.python.coverage.reportPaths=backend/coverage.xml
+ -Dsonar.python.version=3.10
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ - name: Wait for analysis
+ id: qg
+ uses: SonarSource/sonarqube-quality-gate-action@master
+ timeout-minutes: 5
+ continue-on-error: true
+ with:
+ scanMetadataReportFile: project/.scannerwork/report-task.txt
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ # =====================================================
+ # Voller Projekt-Report ins Job-Summary
+ # =====================================================
+ - name: Render Full Metrics
+ if: always()
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_HOST: https://sonarcloud.io
+ QG_STATUS: ${{ steps.qg.outputs.quality-gate-status }}
+ run: |
+ set +e
+ KEY="GalacticCodeGambit_LazyCook"
+ OUT="$GITHUB_STEP_SUMMARY"
+
+ # Alle relevanten Branch-Metriken auf einen Schwung holen
+ METRICS="alert_status,bugs,vulnerabilities,code_smells,security_hotspots,security_hotspots_reviewed,coverage,line_coverage,branch_coverage,lines_to_cover,uncovered_lines,duplicated_lines_density,duplicated_blocks,duplicated_files,duplicated_lines,complexity,cognitive_complexity,classes,functions,statements,files,ncloc,comment_lines,comment_lines_density,sqale_rating,reliability_rating,security_rating,security_review_rating,sqale_index,sqale_debt_ratio,reliability_remediation_effort,security_remediation_effort"
+
+ URL="${SONAR_HOST}/api/measures/component?component=${KEY}&metricKeys=${METRICS}"
+ RESP=$(curl -s -u "${SONAR_TOKEN}:" "${URL}")
+
+ # Mini-Diagnose im Log
+ echo "Anfrage: ${URL}"
+ echo "Response (erste 300 Zeichen):"
+ echo "${RESP}" | head -c 300
+ echo ""
+
+ # Hilfsfunktion: Metrik-Wert holen, "-" als Fallback
+ get() {
+ echo "${RESP}" | jq -r --arg k "$1" \
+ '(.component.measures[]? | select(.metric==$k) | .value) // "-"' 2>/dev/null
+ }
+
+ # Rating-Buchstabe (1.0..5.0 → A..E)
+ rating() {
+ case "$(get "$1")" in
+ 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
+ 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
+ esac
+ }
+
+ # Minuten → Stunden + Minuten
+ fmt_debt() {
+ local m=$1
+ if [[ "$m" =~ ^[0-9]+$ ]]; then
+ local h=$((m / 60)); local r=$((m % 60))
+ echo "${h}h ${r}min"
+ else
+ echo "-"
+ fi
+ }
+
+ # Quality-Gate-Icon
+ if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
+ QG_ICON=":white_check_mark:"; QG_TXT="PASSED"
+ else
+ QG_ICON=":x:"; QG_TXT="FAILED"
+ fi
+
+ DEBT_FMT=$(fmt_debt "$(get sqale_index)")
+ REL_EFF_FMT=$(fmt_debt "$(get reliability_remediation_effort)")
+ SEC_EFF_FMT=$(fmt_debt "$(get security_remediation_effort)")
+
+ # ===== Summary schreiben =====
+ {
+ echo "# SonarCloud – Voller Projekt-Report"
+ echo ""
+ echo "**Quality Gate:** ${QG_ICON} **${QG_TXT}** "
+ echo "**Branch:** ${GITHUB_REF_NAME} "
+ echo "**Commit:** ${GITHUB_SHA:0:7} "
+ echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${KEY}](${SONAR_HOST}/project/overview?id=${KEY})"
+ echo ""
+
+ # --- Übersicht ---
+ echo "## Projekt-Übersicht"
+ echo ""
+ echo "| Kennzahl | Wert |"
+ echo "|---|---:|"
+ echo "| Lines of Code | $(get ncloc) |"
+ echo "| Kommentar-Zeilen | $(get comment_lines) |"
+ echo "| Kommentar-Anteil | $(get comment_lines_density)% |"
+ echo "| Dateien | $(get files) |"
+ echo "| Klassen | $(get classes) |"
+ echo "| Funktionen | $(get functions) |"
+ echo "| Statements | $(get statements) |"
+ echo ""
+
+ # --- Coverage ---
+ echo "## Test-Coverage"
+ echo ""
+ echo "| Metrik | Wert |"
+ echo "|---|---:|"
+ echo "| Coverage (gesamt) | $(get coverage)% |"
+ echo "| Line Coverage | $(get line_coverage)% |"
+ echo "| Branch Coverage | $(get branch_coverage)% |"
+ echo "| Deckbare Zeilen | $(get lines_to_cover) |"
+ echo "| Ungedeckte Zeilen | $(get uncovered_lines) |"
+ echo ""
+
+ # --- Duplikate ---
+ echo "## Duplikate"
+ echo ""
+ echo "| Metrik | Wert |"
+ echo "|---|---:|"
+ echo "| Duplizierte Zeilen (Rate) | $(get duplicated_lines_density)% |"
+ echo "| Duplizierte Zeilen (absolut) | $(get duplicated_lines) |"
+ echo "| Duplikat-Blöcke | $(get duplicated_blocks) |"
+ echo "| Betroffene Dateien | $(get duplicated_files) |"
+ echo ""
+
+ # --- Komplexität ---
+ echo "## Komplexität"
+ echo ""
+ echo "| Metrik | Wert |"
+ echo "|---|---:|"
+ echo "| Zyklomatische Komplexität | $(get complexity) |"
+ echo "| Cognitive Complexity | $(get cognitive_complexity) |"
+ echo ""
+
+ # --- Issues & Ratings ---
+ echo "## Issues & Bewertungen"
+ echo ""
+ echo "| Kategorie | Rating | Anzahl | Behebungs-Aufwand |"
+ echo "|---|:---:|---:|---:|"
+ echo "| Reliability (Bugs) | $(rating reliability_rating) | $(get bugs) | ${REL_EFF_FMT} |"
+ echo "| Security (Vulnerabilities) | $(rating security_rating) | $(get vulnerabilities) | ${SEC_EFF_FMT} |"
+ echo "| Maintainability (Code Smells) | $(rating sqale_rating) | $(get code_smells) | ${DEBT_FMT} |"
+ echo "| Security Hotspots | $(rating security_review_rating) | $(get security_hotspots) | $(get security_hotspots_reviewed)% reviewed |"
+ echo ""
+
+ # --- Technische Schuld ---
+ echo "## Technische Schuld"
+ echo ""
+ echo "| Metrik | Wert |"
+ echo "|---|---:|"
+ echo "| Gesamte Schuld | ${DEBT_FMT} |"
+ echo "| Debt Ratio | $(get sqale_debt_ratio)% |"
+ echo ""
+ } >> "$OUT"
+
+ # =====================================================
+ # Top-10 Files: höchste Komplexität, schlechteste Coverage
+ # =====================================================
+ {
+ echo "## Top-10 komplexeste Dateien"
+ echo ""
+ echo "| Datei | Komplexität | Cognitive | LoC |"
+ echo "|---|---:|---:|---:|"
+ } >> "$OUT"
+
+ curl -s -u "${SONAR_TOKEN}:" \
+ "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=complexity,cognitive_complexity,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=complexity&asc=false" \
+ | jq -r '.components[]?
+ | (.path // .key) as $p
+ | ((.measures[]? | select(.metric=="complexity") | .value) // "-") as $c
+ | ((.measures[]? | select(.metric=="cognitive_complexity")| .value) // "-") as $cog
+ | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
+ | "| " + $p + " | " + $c + " | " + $cog + " | " + $n + " |"' \
+ >> "$OUT"
+
+ {
+ echo ""
+ echo "## Top-10 Dateien mit schlechtester Coverage"
+ echo ""
+ echo "| Datei | Coverage | Ungedeckte Zeilen | LoC |"
+ echo "|---|---:|---:|---:|"
+ } >> "$OUT"
+
+ curl -s -u "${SONAR_TOKEN}:" \
+ "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=coverage,uncovered_lines,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=coverage&asc=true" \
+ | jq -r '.components[]?
+ | select(.measures[]? | select(.metric=="coverage"))
+ | (.path // .key) as $p
+ | ((.measures[]? | select(.metric=="coverage") | .value) // "-") as $cov
+ | ((.measures[]? | select(.metric=="uncovered_lines") | .value) // "-") as $unc
+ | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
+ | "| " + $p + " | " + $cov + "% | " + $unc + " | " + $n + " |"' \
+ >> "$OUT"
+
+ {
+ echo ""
+ echo "## Top-10 Dateien mit den meisten Duplikat-Blöcken"
+ echo ""
+ echo "| Datei | Blöcke | Dupl. Zeilen | LoC |"
+ echo "|---|---:|---:|---:|"
+ } >> "$OUT"
+
+ curl -s -u "${SONAR_TOKEN}:" \
+ "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=duplicated_blocks,duplicated_lines,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=duplicated_blocks&asc=false" \
+ | jq -r '.components[]?
+ | (.path // .key) as $p
+ | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
+ | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
+ | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
+ | select(($b | tonumber) > 0)
+ | "| " + $p + " | " + $b + " | " + $l + " | " + $n + " |"' \
+ >> "$OUT"
+
+ {
+ echo ""
+ echo "---"
+ echo "_Generiert von SonarCloud-Analyse aus Push auf \`${GITHUB_REF_NAME}\`._"
+ } >> "$OUT"
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index e3a0155..4a9a6c5 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -2,9 +2,9 @@
on:
push:
- branches: [main, master, develop]
+ branches: [main, master, sonarqube]
pull_request:
- branches: [main, master, develop]
+ branches: [main, master]
workflow_dispatch:
permissions:
From da348f0210432e095b2e0a435aa6492a736a54d1 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 10:47:52 +0200
Subject: [PATCH 69/85] Test: Bash Print in CI/CD - Pipeline with detailed
information about coverage, duplication and complexity
---
.github/workflows/sonarqube-main.yml | 37 ++++++++++++++++++++++++----
1 file changed, 32 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/sonarqube-main.yml b/.github/workflows/sonarqube-main.yml
index 81dfb69..9c29091 100644
--- a/.github/workflows/sonarqube-main.yml
+++ b/.github/workflows/sonarqube-main.yml
@@ -80,17 +80,44 @@ jobs:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST: https://sonarcloud.io
QG_STATUS: ${{ steps.qg.outputs.quality-gate-status }}
+ EVENT_NAME: ${{ github.event_name }}
+ BRANCH_NAME: ${{ github.ref_name }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set +e
KEY="GalacticCodeGambit_LazyCook"
OUT="$GITHUB_STEP_SUMMARY"
- # Alle relevanten Branch-Metriken auf einen Schwung holen
+ # ---- Scope der API-Abfrage dynamisch bestimmen ----
+ # PR-Lauf → ?pullRequest=
+ # Push auf nicht-Default-Branch → ?branch=
+ # Push auf main/master → kein Parameter (Default-Branch)
+ SCOPE_PARAM=""
+ SCOPE_DESC="Default-Branch"
+ if [ -n "${PR_NUMBER}" ]; then
+ SCOPE_PARAM="&pullRequest=${PR_NUMBER}"
+ SCOPE_DESC="PR #${PR_NUMBER}"
+ elif [ "${BRANCH_NAME}" != "main" ] && [ "${BRANCH_NAME}" != "master" ]; then
+ SCOPE_PARAM="&branch=${BRANCH_NAME}"
+ SCOPE_DESC="Branch ${BRANCH_NAME}"
+ fi
+ echo "Abfrage-Scope: ${SCOPE_DESC}"
+
+ # Alle relevanten Metriken auf einen Schwung holen
METRICS="alert_status,bugs,vulnerabilities,code_smells,security_hotspots,security_hotspots_reviewed,coverage,line_coverage,branch_coverage,lines_to_cover,uncovered_lines,duplicated_lines_density,duplicated_blocks,duplicated_files,duplicated_lines,complexity,cognitive_complexity,classes,functions,statements,files,ncloc,comment_lines,comment_lines_density,sqale_rating,reliability_rating,security_rating,security_review_rating,sqale_index,sqale_debt_ratio,reliability_remediation_effort,security_remediation_effort"
- URL="${SONAR_HOST}/api/measures/component?component=${KEY}&metricKeys=${METRICS}"
+ URL="${SONAR_HOST}/api/measures/component?component=${KEY}&metricKeys=${METRICS}${SCOPE_PARAM}"
RESP=$(curl -s -u "${SONAR_TOKEN}:" "${URL}")
+ # Falls die Scope-Abfrage leer ist: Fallback auf Default-Branch
+ HAS=$(echo "${RESP}" | jq -r '(.component.measures | length) // 0' 2>/dev/null)
+ if [ -z "${HAS}" ] || [ "${HAS}" = "0" ]; then
+ echo "Scope-Abfrage leer, Fallback auf Default-Branch"
+ URL="${SONAR_HOST}/api/measures/component?component=${KEY}&metricKeys=${METRICS}"
+ RESP=$(curl -s -u "${SONAR_TOKEN}:" "${URL}")
+ SCOPE_DESC="${SCOPE_DESC} → fallback Default-Branch"
+ fi
+
# Mini-Diagnose im Log
echo "Anfrage: ${URL}"
echo "Response (erste 300 Zeichen):"
@@ -221,7 +248,7 @@ jobs:
} >> "$OUT"
curl -s -u "${SONAR_TOKEN}:" \
- "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=complexity,cognitive_complexity,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=complexity&asc=false" \
+ "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=complexity,cognitive_complexity,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=complexity&asc=false${SCOPE_PARAM}" \
| jq -r '.components[]?
| (.path // .key) as $p
| ((.measures[]? | select(.metric=="complexity") | .value) // "-") as $c
@@ -239,7 +266,7 @@ jobs:
} >> "$OUT"
curl -s -u "${SONAR_TOKEN}:" \
- "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=coverage,uncovered_lines,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=coverage&asc=true" \
+ "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=coverage,uncovered_lines,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=coverage&asc=true${SCOPE_PARAM}" \
| jq -r '.components[]?
| select(.measures[]? | select(.metric=="coverage"))
| (.path // .key) as $p
@@ -258,7 +285,7 @@ jobs:
} >> "$OUT"
curl -s -u "${SONAR_TOKEN}:" \
- "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=duplicated_blocks,duplicated_lines,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=duplicated_blocks&asc=false" \
+ "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=duplicated_blocks,duplicated_lines,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=duplicated_blocks&asc=false${SCOPE_PARAM}" \
| jq -r '.components[]?
| (.path // .key) as $p
| ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
From ff40d5eb68302e34b97981dddce5ebf953e5aca8 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 10:53:43 +0200
Subject: [PATCH 70/85] Test: Bash Print in CI/CD - Pipeline with detailed
information about coverage, duplication and complexity
---
.github/workflows/sonarqube-main.yml | 44 ++++++----------------------
1 file changed, 9 insertions(+), 35 deletions(-)
diff --git a/.github/workflows/sonarqube-main.yml b/.github/workflows/sonarqube-main.yml
index 9c29091..ac265e2 100644
--- a/.github/workflows/sonarqube-main.yml
+++ b/.github/workflows/sonarqube-main.yml
@@ -2,9 +2,9 @@ name: SonarCloud Full Metrics (main branch)
on:
push:
- branches: [main, master, sonarqube]
+ branches: [sonarqube]
pull_request:
- branches: [main, master, sonarqube]
+ branches: [main, master]
workflow_dispatch:
permissions:
@@ -42,10 +42,11 @@ jobs:
python -m pytest \
--cov=backend \
--cov-report=xml:backend/coverage.xml \
+ --cov-report=term-missing \
backend/tests/
continue-on-error: true
- # =====================================================
+ # =====================================================
# SonarCloud Scan
# =====================================================
- name: SonarQube Scan
@@ -80,44 +81,17 @@ jobs:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST: https://sonarcloud.io
QG_STATUS: ${{ steps.qg.outputs.quality-gate-status }}
- EVENT_NAME: ${{ github.event_name }}
- BRANCH_NAME: ${{ github.ref_name }}
- PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set +e
KEY="GalacticCodeGambit_LazyCook"
OUT="$GITHUB_STEP_SUMMARY"
- # ---- Scope der API-Abfrage dynamisch bestimmen ----
- # PR-Lauf → ?pullRequest=
- # Push auf nicht-Default-Branch → ?branch=
- # Push auf main/master → kein Parameter (Default-Branch)
- SCOPE_PARAM=""
- SCOPE_DESC="Default-Branch"
- if [ -n "${PR_NUMBER}" ]; then
- SCOPE_PARAM="&pullRequest=${PR_NUMBER}"
- SCOPE_DESC="PR #${PR_NUMBER}"
- elif [ "${BRANCH_NAME}" != "main" ] && [ "${BRANCH_NAME}" != "master" ]; then
- SCOPE_PARAM="&branch=${BRANCH_NAME}"
- SCOPE_DESC="Branch ${BRANCH_NAME}"
- fi
- echo "Abfrage-Scope: ${SCOPE_DESC}"
-
- # Alle relevanten Metriken auf einen Schwung holen
+ # Alle relevanten Branch-Metriken auf einen Schwung holen
METRICS="alert_status,bugs,vulnerabilities,code_smells,security_hotspots,security_hotspots_reviewed,coverage,line_coverage,branch_coverage,lines_to_cover,uncovered_lines,duplicated_lines_density,duplicated_blocks,duplicated_files,duplicated_lines,complexity,cognitive_complexity,classes,functions,statements,files,ncloc,comment_lines,comment_lines_density,sqale_rating,reliability_rating,security_rating,security_review_rating,sqale_index,sqale_debt_ratio,reliability_remediation_effort,security_remediation_effort"
- URL="${SONAR_HOST}/api/measures/component?component=${KEY}&metricKeys=${METRICS}${SCOPE_PARAM}"
+ URL="${SONAR_HOST}/api/measures/component?component=${KEY}&metricKeys=${METRICS}"
RESP=$(curl -s -u "${SONAR_TOKEN}:" "${URL}")
- # Falls die Scope-Abfrage leer ist: Fallback auf Default-Branch
- HAS=$(echo "${RESP}" | jq -r '(.component.measures | length) // 0' 2>/dev/null)
- if [ -z "${HAS}" ] || [ "${HAS}" = "0" ]; then
- echo "Scope-Abfrage leer, Fallback auf Default-Branch"
- URL="${SONAR_HOST}/api/measures/component?component=${KEY}&metricKeys=${METRICS}"
- RESP=$(curl -s -u "${SONAR_TOKEN}:" "${URL}")
- SCOPE_DESC="${SCOPE_DESC} → fallback Default-Branch"
- fi
-
# Mini-Diagnose im Log
echo "Anfrage: ${URL}"
echo "Response (erste 300 Zeichen):"
@@ -248,7 +222,7 @@ jobs:
} >> "$OUT"
curl -s -u "${SONAR_TOKEN}:" \
- "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=complexity,cognitive_complexity,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=complexity&asc=false${SCOPE_PARAM}" \
+ "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=complexity,cognitive_complexity,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=complexity&asc=false" \
| jq -r '.components[]?
| (.path // .key) as $p
| ((.measures[]? | select(.metric=="complexity") | .value) // "-") as $c
@@ -266,7 +240,7 @@ jobs:
} >> "$OUT"
curl -s -u "${SONAR_TOKEN}:" \
- "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=coverage,uncovered_lines,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=coverage&asc=true${SCOPE_PARAM}" \
+ "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=coverage,uncovered_lines,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=coverage&asc=true" \
| jq -r '.components[]?
| select(.measures[]? | select(.metric=="coverage"))
| (.path // .key) as $p
@@ -285,7 +259,7 @@ jobs:
} >> "$OUT"
curl -s -u "${SONAR_TOKEN}:" \
- "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=duplicated_blocks,duplicated_lines,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=duplicated_blocks&asc=false${SCOPE_PARAM}" \
+ "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=duplicated_blocks,duplicated_lines,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=duplicated_blocks&asc=false" \
| jq -r '.components[]?
| (.path // .key) as $p
| ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
From 5d072594d9a42fd906954e06a3455f4efa946377 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 11:01:07 +0200
Subject: [PATCH 71/85] Test: Bash Print in CI/CD - Pipeline with detailed
information about coverage, duplication and complexity
---
.github/workflows/sonarqube-main.yml | 89 ++++++++++++++++++++++------
1 file changed, 70 insertions(+), 19 deletions(-)
diff --git a/.github/workflows/sonarqube-main.yml b/.github/workflows/sonarqube-main.yml
index ac265e2..9a7ad24 100644
--- a/.github/workflows/sonarqube-main.yml
+++ b/.github/workflows/sonarqube-main.yml
@@ -134,6 +134,35 @@ jobs:
REL_EFF_FMT=$(fmt_debt "$(get reliability_remediation_effort)")
SEC_EFF_FMT=$(fmt_debt "$(get security_remediation_effort)")
+ # ===== Backend-Coverage direkt aus coverage.xml =====
+ COV_FILE="project/backend/coverage.xml"
+ if [ -f "${COV_FILE}" ]; then
+ COV_DATA=$(python3 - "${COV_FILE}" <<'PYEOF'
+ import sys, xml.etree.ElementTree as ET
+ root = ET.parse(sys.argv[1]).getroot()
+ lr = float(root.get('line-rate', '0'))
+ br = float(root.get('branch-rate', '0'))
+ lv = int(root.get('lines-valid', '0'))
+ lc = int(root.get('lines-covered', '0'))
+ bv = int(root.get('branches-valid', '0'))
+ bc = int(root.get('branches-covered', '0'))
+ total = lv + bv
+ covered = lc + bc
+ overall = (covered / total * 100) if total > 0 else 0.0
+ uncov_lines = lv - lc
+ uncov_branches = bv - bc
+ print(f"{overall:.1f} {lr*100:.1f} {br*100:.1f} {lv} {lc} {bv} {bc} {uncov_lines} {uncov_branches}")
+ PYEOF
+ )
+ read -r COV_OVERALL COV_LINE COV_BRANCH LINES_VALID LINES_COVERED BRANCHES_VALID BRANCHES_COVERED UNCOVERED_LINES UNCOVERED_BRANCHES <<< "${COV_DATA}"
+ echo "Backend-Coverage aus coverage.xml: ${COV_OVERALL}% (Line ${COV_LINE}%, Branch ${COV_BRANCH}%)"
+ else
+ echo "::warning::coverage.xml nicht gefunden bei ${COV_FILE}"
+ COV_OVERALL="-"; COV_LINE="-"; COV_BRANCH="-"
+ LINES_VALID="-"; LINES_COVERED="-"; BRANCHES_VALID="-"; BRANCHES_COVERED="-"
+ UNCOVERED_LINES="-"; UNCOVERED_BRANCHES="-"
+ fi
+
# ===== Summary schreiben =====
{
echo "# SonarCloud – Voller Projekt-Report"
@@ -158,16 +187,22 @@ jobs:
echo "| Statements | $(get statements) |"
echo ""
- # --- Coverage ---
- echo "## Test-Coverage"
+ # --- Coverage (aus pytest-cov coverage.xml, nicht aus Sonar-API) ---
+ echo "## Test-Coverage (Backend / Python)"
+ echo ""
+ echo "_Werte direkt aus \`backend/coverage.xml\` von pytest-cov gelesen._"
echo ""
echo "| Metrik | Wert |"
echo "|---|---:|"
- echo "| Coverage (gesamt) | $(get coverage)% |"
- echo "| Line Coverage | $(get line_coverage)% |"
- echo "| Branch Coverage | $(get branch_coverage)% |"
- echo "| Deckbare Zeilen | $(get lines_to_cover) |"
- echo "| Ungedeckte Zeilen | $(get uncovered_lines) |"
+ echo "| Coverage (gesamt) | ${COV_OVERALL}% |"
+ echo "| Line Coverage | ${COV_LINE}% |"
+ echo "| Branch Coverage | ${COV_BRANCH}% |"
+ echo "| Deckbare Zeilen | ${LINES_VALID} |"
+ echo "| Gedeckte Zeilen | ${LINES_COVERED} |"
+ echo "| Ungedeckte Zeilen | ${UNCOVERED_LINES} |"
+ echo "| Deckbare Branches | ${BRANCHES_VALID} |"
+ echo "| Gedeckte Branches | ${BRANCHES_COVERED} |"
+ echo "| Ungedeckte Branches | ${UNCOVERED_BRANCHES} |"
echo ""
# --- Duplikate ---
@@ -233,22 +268,38 @@ jobs:
{
echo ""
- echo "## Top-10 Dateien mit schlechtester Coverage"
+ echo "## Top-10 Dateien mit schlechtester Coverage (Backend)"
+ echo ""
+ echo "_Aus \`backend/coverage.xml\`, sortiert aufsteigend nach Coverage._"
echo ""
- echo "| Datei | Coverage | Ungedeckte Zeilen | LoC |"
+ echo "| Datei | Coverage | Gedeckte / Deckbare | Ungedeckte Zeilen |"
echo "|---|---:|---:|---:|"
} >> "$OUT"
- curl -s -u "${SONAR_TOKEN}:" \
- "${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=coverage,uncovered_lines,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=coverage&asc=true" \
- | jq -r '.components[]?
- | select(.measures[]? | select(.metric=="coverage"))
- | (.path // .key) as $p
- | ((.measures[]? | select(.metric=="coverage") | .value) // "-") as $cov
- | ((.measures[]? | select(.metric=="uncovered_lines") | .value) // "-") as $unc
- | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
- | "| " + $p + " | " + $cov + "% | " + $unc + " | " + $n + " |"' \
- >> "$OUT"
+ if [ -f "${COV_FILE}" ]; then
+ python3 - "${COV_FILE}" >> "$OUT" <<'PYEOF'
+ import sys, xml.etree.ElementTree as ET
+ root = ET.parse(sys.argv[1]).getroot()
+ rows = []
+ for cls in root.iter('class'):
+ fname = cls.get('filename', '')
+ lines = cls.find('lines')
+ if lines is None:
+ continue
+ total = len(lines.findall('line'))
+ if total == 0:
+ continue
+ covered = sum(1 for l in lines.findall('line') if int(l.get('hits', '0')) > 0)
+ uncov = total - covered
+ pct = covered / total * 100
+ rows.append((pct, fname, covered, total, uncov))
+ rows.sort(key=lambda r: (r[0], -r[3]))
+ for pct, fname, cov, total, uncov in rows[:10]:
+ print(f"| {fname} | {pct:.1f}% | {cov} / {total} | {uncov} |")
+ PYEOF
+ else
+ echo "_coverage.xml nicht gefunden._" >> "$OUT"
+ fi
{
echo ""
From da3856b9c5cbb4f6bf900e868a3bcafee8b7ed3f Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 11:07:35 +0200
Subject: [PATCH 72/85] Test: Bash Print in CI/CD - Pipeline with detailed
information about coverage, duplication and complexity
---
.github/workflows/sonarqube-main.yml | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/sonarqube-main.yml b/.github/workflows/sonarqube-main.yml
index 9a7ad24..035362d 100644
--- a/.github/workflows/sonarqube-main.yml
+++ b/.github/workflows/sonarqube-main.yml
@@ -268,9 +268,9 @@ jobs:
{
echo ""
- echo "## Top-10 Dateien mit schlechtester Coverage (Backend)"
+ echo "## Coverage-Übersicht aller Backend-Dateien"
echo ""
- echo "_Aus \`backend/coverage.xml\`, sortiert aufsteigend nach Coverage._"
+ echo "_Aus \`backend/coverage.xml\`, sortiert aufsteigend nach Coverage (schlechteste zuerst)._"
echo ""
echo "| Datei | Coverage | Gedeckte / Deckbare | Ungedeckte Zeilen |"
echo "|---|---:|---:|---:|"
@@ -293,9 +293,12 @@ jobs:
uncov = total - covered
pct = covered / total * 100
rows.append((pct, fname, covered, total, uncov))
+ # Aufsteigend nach Coverage, bei Gleichstand größere Dateien zuerst
rows.sort(key=lambda r: (r[0], -r[3]))
- for pct, fname, cov, total, uncov in rows[:10]:
+ for pct, fname, cov, total, uncov in rows:
print(f"| {fname} | {pct:.1f}% | {cov} / {total} | {uncov} |")
+ print()
+ print(f"_Insgesamt {len(rows)} Datei(en) mit Coverage-Daten._")
PYEOF
else
echo "_coverage.xml nicht gefunden._" >> "$OUT"
From 5cc69dbafcb740b72a2679c903fc2bb1c5de49f3 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Thu, 21 May 2026 11:13:32 +0200
Subject: [PATCH 73/85] Test: Bash Print in CI/CD - Pipeline with detailed
information about coverage, duplication and complexity
---
.github/workflows/sonarqube-main.yml | 2 --
.github/workflows/sonarqube.yml | 2 --
2 files changed, 4 deletions(-)
diff --git a/.github/workflows/sonarqube-main.yml b/.github/workflows/sonarqube-main.yml
index 035362d..986c1dd 100644
--- a/.github/workflows/sonarqube-main.yml
+++ b/.github/workflows/sonarqube-main.yml
@@ -1,8 +1,6 @@
name: SonarCloud Full Metrics (main branch)
on:
- push:
- branches: [sonarqube]
pull_request:
branches: [main, master]
workflow_dispatch:
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 4a9a6c5..22ff9bb 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -1,8 +1,6 @@
name: SonarCloud Analysis
on:
- push:
- branches: [main, master, sonarqube]
pull_request:
branches: [main, master]
workflow_dispatch:
From f7278d4c621ea21c49418f444896d71687bd59d7 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 26 May 2026 12:35:10 +0200
Subject: [PATCH 74/85] Fix: Code Preview from Copilot fixes
---
README.md | 4 +-
project/backend/Auth.py | 4 --
project/backend/Database.py | 10 ++--
project/backend/EmailService.py | 39 --------------
project/backend/Ingredient.py | 1 +
project/backend/Recipe.py | 4 +-
project/backend/RecipeSUCUK.py | 23 ++++----
project/frontend/app/homepage/homepage.tsx | 1 -
.../frontend/app/profile/changeEmailPopup.tsx | 3 +-
.../app/profile/changePasswordPopup.tsx | 52 +++++--------------
project/frontend/app/profile/page.tsx | 5 +-
project/frontend/lib/auth.tsx | 15 +++---
12 files changed, 47 insertions(+), 114 deletions(-)
diff --git a/README.md b/README.md
index 66a742c..e0db6b5 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ LazyCook soll eine Webanwendung sein. Die es ermöglichen seine vorhanden Zutate
1. Klonen das Repository:
`git clone https://github.com/GalacticCodeGambit/LazyCook.git`
2. Navigieren zum Projektverzeichnis:
- `cd LazyCook/Project`
+ `cd LazyCook/project`
3. Starten der Anwendung mit: Docker Compose:
`docker compose up --build -d`
@@ -34,7 +34,7 @@ Die Anwendung sollte jetzt unter `http://localhost:8000` erreichbar sein.
Für die Funktion [#115](https://github.com/GalacticCodeGambit/LazyCook/issues/115) von Email Versenden/Empfangen muss im Ordner `project/` eine `.env` Datei mit den folgenden Variablen angelegt werden:
```
-EMAIL_HOST=
+GMAIL_USER=
GMAIL_PASSWORD=
```
diff --git a/project/backend/Auth.py b/project/backend/Auth.py
index 3c8dc46..2447716 100644
--- a/project/backend/Auth.py
+++ b/project/backend/Auth.py
@@ -16,10 +16,6 @@
PASSWORD_RESET_EXPIRE_MINUTES = 30
-import hashlib
-
-PASSWORD_RESET_EXPIRE_MINUTES = 30
-
from Models import Token, User
from Database import getAccountByEmail, saveRefreshToken, getRefreshToken
diff --git a/project/backend/Database.py b/project/backend/Database.py
index 580ad41..69ad3c9 100644
--- a/project/backend/Database.py
+++ b/project/backend/Database.py
@@ -247,7 +247,7 @@ def addRecipe(name: str, description: str, vid: int) -> int:
return cur.lastrowid
-def addIngredientToRecipe(zid: int, rid: int, amount: float) -> int:
+def addIngredientToRecipe(zid: int, rid: int, amount: float) -> None:
with getDB() as con:
cur = con.cursor()
cur.execute(
@@ -272,8 +272,8 @@ def getIngridientByName(name: str):
cur.execute(
"""
SELECT id, amountType
- FROM Recipe
- WHERE id = ?
+ FROM Ingredient
+ WHERE name = ?
""",
(name),
)
@@ -336,8 +336,8 @@ def getAllocatedRecipes(name: str) -> list[dict]:
"""
SELECT rid
FROM Exists_from
- Inner Join Ingredient on rid=id,
- Where Exists_from.id = ?
+ Inner Join Ingredient on rid=id
+ Where Ingrediant.name = ?
""",
(name,),
)
diff --git a/project/backend/EmailService.py b/project/backend/EmailService.py
index d978e0d..324b2b8 100644
--- a/project/backend/EmailService.py
+++ b/project/backend/EmailService.py
@@ -73,42 +73,3 @@ def sendPasswordResetEmail(to_email: str, name: str, resetLink: str) -> None:
print(f"E-Mail Fehler: {e}")
raise
-
-def sendPasswordResetEmail(to_email: str, name: str, resetLink: str) -> None:
- try:
- msg = MIMEMultipart("alternative")
- msg["Subject"] = "Passwort zurücksetzen – Lazy Cook"
- msg["From"] = gmailUser
- msg["To"] = to_email
-
- html = f"""
-
-
Hallo {name},
-
du hast angefordert, dein Passwort bei Lazy Cook zurückzusetzen.
-
Klicke auf den Button, um ein neues Passwort festzulegen:
-
-
- Passwort zurücksetzen
-
-
-
- Oder kopiere diesen Link in deinen Browser:
- {resetLink}
-
-
Der Link ist 30 Minuten gültig.
-
Falls du das nicht angefordert hast, ignoriere diese E-Mail einfach – dein Passwort bleibt unverändert.
-
-
– Das Lazy Cook Team
-
- """
- msg.attach(MIMEText(html, "html"))
-
- with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
- server.login(gmailUser, gmailPassword)
- server.sendmail(gmailUser, to_email, msg.as_string())
-
- except Exception as e:
- print(f"E-Mail Fehler: {e}")
- raise
diff --git a/project/backend/Ingredient.py b/project/backend/Ingredient.py
index 154caba..7014e22 100644
--- a/project/backend/Ingredient.py
+++ b/project/backend/Ingredient.py
@@ -6,6 +6,7 @@ class Ingredient:
def __init__(self, name: str, amount: float):
self.__name = name
self.__amount = amount
+ self.__amountType = None
def getName(self) -> str:
return self.__name
diff --git a/project/backend/Recipe.py b/project/backend/Recipe.py
index b9ce137..5c0ca46 100644
--- a/project/backend/Recipe.py
+++ b/project/backend/Recipe.py
@@ -16,7 +16,7 @@ def __init__(self, name: str, ingredients: list[Ingredient], description: str):
def saveInDB(self) -> bool:
rid = addRecipe(self.__name, self.__description, None)
for ingridient in self.__ingredients:
- zid = getIngridientByName(ingridient.getName())
+ zid = getIngridientByName(ingridient.getName())['id']
if not zid:
return False
else:
@@ -59,7 +59,7 @@ def getDuration(self) -> str:
def setDuration(self, duration: str):
self.__duration = duration
- def getingredients(self) -> list[Ingredient]:
+ def getIngredients(self) -> list[Ingredient]:
return self.__ingredients
def setIngredient(self, ingredients: list[Ingredient]):
diff --git a/project/backend/RecipeSUCUK.py b/project/backend/RecipeSUCUK.py
index b41c3e6..d3de8f0 100644
--- a/project/backend/RecipeSUCUK.py
+++ b/project/backend/RecipeSUCUK.py
@@ -49,14 +49,15 @@ def __formatIngredients(id: int) -> list[Ingredient]:
return Ingredients
-ini = []
-ini.append(Ingredient("Linguine", 10))
-ini.append(Ingredient("Fresh Basil", 10))
-ini.append(Ingredient("Pine Nuts", 10))
-ini.append(Ingredient("Parmesan", 10))
-ini.append(Ingredient("Ground Beef", 10))
-ini.append(Ingredient("Cumin", 10))
-ini.append(Ingredient("Cucumber", 10))
-arr = findRecipes(ini)
-for i in arr:
- print(i.getName() + " " + str(i.getRating()))
+if __name__ == "__main__":
+ ini = []
+ ini.append(Ingredient("Linguine", 10))
+ ini.append(Ingredient("Fresh Basil", 10))
+ ini.append(Ingredient("Pine Nuts", 10))
+ ini.append(Ingredient("Parmesan", 10))
+ ini.append(Ingredient("Ground Beef", 10))
+ ini.append(Ingredient("Cumin", 10))
+ ini.append(Ingredient("Cucumber", 10))
+ arr = findRecipes(ini)
+ for i in arr:
+ print(i.getName() + " " + str(i.getRating()))
diff --git a/project/frontend/app/homepage/homepage.tsx b/project/frontend/app/homepage/homepage.tsx
index cb21ae5..52ff7bf 100644
--- a/project/frontend/app/homepage/homepage.tsx
+++ b/project/frontend/app/homepage/homepage.tsx
@@ -28,7 +28,6 @@ export default function Homepage() {
diff --git a/project/frontend/app/profile/changeEmailPopup.tsx b/project/frontend/app/profile/changeEmailPopup.tsx
index 1d1dd3b..b473678 100644
--- a/project/frontend/app/profile/changeEmailPopup.tsx
+++ b/project/frontend/app/profile/changeEmailPopup.tsx
@@ -3,7 +3,6 @@ import {fetchWithAuth} from "@/lib/auth";
import {useState} from "react";
import "../recipeFinder/style.css"
-const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";
export default function ChangeEmail (){
@@ -13,7 +12,7 @@ export default function ChangeEmail (){
async function handleEmailChange() {
setEmailMsg("");
try {
- const res = await fetchWithAuth(`${API_URL}/users/me`, {
+ const res = await fetchWithAuth(`/users/me`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: newEmail }),
diff --git a/project/frontend/app/profile/changePasswordPopup.tsx b/project/frontend/app/profile/changePasswordPopup.tsx
index 0eccf7f..12423c8 100644
--- a/project/frontend/app/profile/changePasswordPopup.tsx
+++ b/project/frontend/app/profile/changePasswordPopup.tsx
@@ -5,14 +5,12 @@ import {Eye, EyeOff} from "lucide-react";
import "../recipeFinder/style.css"
import Field from "@/app/components/fields";
-const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";
-
function PasswordInput({ value, onChange, placeholder, onKeyDown, ariaLabel }: {
- value: string;
- onChange: (v: string) => void;
- placeholder: string;
- onKeyDown?: (e: React.KeyboardEvent) => void;
- ariaLabel?: string;
+ readonly value: string;
+ readonly onChange: (v: string) => void;
+ readonly placeholder: string;
+ readonly onKeyDown?: (e: React.KeyboardEvent) => void;
+ readonly ariaLabel?: string;
}) {
const [show, setShow] = useState(false);
return (
@@ -52,14 +50,11 @@ function PasswordInput({ value, onChange, placeholder, onKeyDown, ariaLabel }: {
);
}
-type Modus = "change" | "forgot";
-
interface ChangePasswordProps {
- modus: Modus;
onSuccess?: () => void;
}
-export default function ChangePassword({ modus, onSuccess }: ChangePasswordProps) {
+export default function ChangePassword({ onSuccess }: Readonly) {
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
@@ -67,13 +62,10 @@ export default function ChangePassword({ modus, onSuccess }: ChangePasswordProps
const [passwordMsg, setPasswordMsg] = useState("");
const [pwBlurred, setPwBlurred] = useState(false);
- const isForgot = modus === "forgot";
-
async function handlePasswordChange() {
setPasswordMsg("");
- // Pflichtfelder im "change"-Modus
- if (!isForgot && !currentPassword) {
+ if (!currentPassword) {
setPasswordMsg("Bitte das aktuelle Passwort eingeben.");
return;
}
@@ -86,27 +78,16 @@ export default function ChangePassword({ modus, onSuccess }: ChangePasswordProps
return;
}
- // Bestätigung prüfen
if (newPassword !== confirmPassword) {
setPasswordMsg("Die Passwörter stimmen nicht überein.");
return;
}
try {
- const endpoint = isForgot
- ? `${API_URL}/users/forgot-password`
- : `${API_URL}/users/me`;
-
- const body = isForgot
- ? { newPassword }
- : { currentPassword, newPassword };
-
- const fetcher = isForgot ? fetch : fetchWithAuth;
-
- const res = await fetcher(endpoint, {
+ const res = await fetchWithAuth("/users/me", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify(body),
+ body: JSON.stringify({ currentPassword, newPassword }),
});
if (!res.ok) {
@@ -115,11 +96,7 @@ export default function ChangePassword({ modus, onSuccess }: ChangePasswordProps
return;
}
- setPasswordMsg(
- isForgot
- ? "Passwort erfolgreich zurückgesetzt."
- : "Passwort erfolgreich geändert."
- );
+ setPasswordMsg("Passwort erfolgreich geändert.");
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
@@ -131,13 +108,10 @@ export default function ChangePassword({ modus, onSuccess }: ChangePasswordProps
return (
-
- {isForgot ? "Passwort zurücksetzen" : "Passwort ändern"}
-
+
Passwort ändern
- {!isForgot && (
-
setPwBlurred(true)} onKeyDown={(e) => e.key === "Enter" && handlePasswordChange()} state={pwBlurred && !currentPassword? "error" : "default"} />)}
+ setPwBlurred(true)} onKeyDown={(e) => e.key === "Enter" && handlePasswordChange()} state={pwBlurred && !currentPassword? "error" : "default"} />
- {isForgot ? "Zurücksetzen" : "Speichern"}
+ Speichern
diff --git a/project/frontend/app/profile/page.tsx b/project/frontend/app/profile/page.tsx
index a6be68f..98df4a6 100644
--- a/project/frontend/app/profile/page.tsx
+++ b/project/frontend/app/profile/page.tsx
@@ -12,7 +12,6 @@ import ChangePassword from "@/app/profile/changePasswordPopup";
import "../recipeFinder/style.css"
-const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000";
export default function Profile() {
const { user, loading, logout } = useAuth();
@@ -40,7 +39,7 @@ export default function Profile() {
if (!user) return null;
async function handleAccountDeletion() {
- const res = await fetchWithAuth(`${API_URL}/users/me`, { method: "DELETE" });
+ const res = await fetchWithAuth(`/users/me`, { method: "DELETE" });
if (!res.ok) throw new Error("Konto löschen fehlgeschlagen");
logout();
router.push("/");
@@ -124,7 +123,7 @@ export default function Profile() {
setShowPasswordModal(false)}>
-
+
);
diff --git a/project/frontend/lib/auth.tsx b/project/frontend/lib/auth.tsx
index 64c79d9..828fe7c 100644
--- a/project/frontend/lib/auth.tsx
+++ b/project/frontend/lib/auth.tsx
@@ -6,6 +6,7 @@ import {
useState,
useEffect,
useCallback,
+ useMemo,
type ReactNode,
} from "react";
@@ -108,7 +109,8 @@ async function fetchWithAuth(url: string, options: RequestInit = {}): Promise(null);
-export function AuthProvider({ children }: { children: ReactNode }) {
+export function AuthProvider({ children }: Readonly<{ children: ReactNode }>) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
@@ -195,8 +197,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setUser(null);
}
};
- window.addEventListener("storage", handleStorageChange);
- return () => window.removeEventListener("storage", handleStorageChange);
+ globalThis.addEventListener("storage", handleStorageChange);
+ return () => globalThis.removeEventListener("storage", handleStorageChange);
}, []);
const login = useCallback(async (email: string, password: string) => {
@@ -220,8 +222,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setUser(null);
}, []);
+ const obj = useMemo(() => ({ user, loading, login, register, logout }), []);
return (
-
+
{children}
);
From 24359c21510baf6a095ccf2fb1d8855b8147cee5 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 26 May 2026 12:39:39 +0200
Subject: [PATCH 75/85] Fix: Code Preview from Copilot fixes
---
project/frontend/lib/auth.tsx | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/project/frontend/lib/auth.tsx b/project/frontend/lib/auth.tsx
index 828fe7c..0fc4026 100644
--- a/project/frontend/lib/auth.tsx
+++ b/project/frontend/lib/auth.tsx
@@ -222,9 +222,8 @@ export function AuthProvider({ children }: Readonly<{ children: ReactNode }>) {
setUser(null);
}, []);
- const obj = useMemo(() => ({ user, loading, login, register, logout }), []);
- return (
-
+ return (
+
{children}
);
From b3b683ac05ef0bc45f3240400d706d1f3918a175 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 26 May 2026 13:11:33 +0200
Subject: [PATCH 76/85] Fix: Code Preview from Copilot fixes
---
.github/workflows/sonarqube-main.yml | 15 ++++++++++-----
.github/workflows/sonarqube.yml | 13 +++++++++----
project/backend/Database.py | 2 +-
project/backend/Recipe.py | 8 ++++----
project/frontend/lib/auth.tsx | 3 +--
project/sonar-project.properties | 4 ++--
6 files changed, 27 insertions(+), 18 deletions(-)
diff --git a/.github/workflows/sonarqube-main.yml b/.github/workflows/sonarqube-main.yml
index 986c1dd..ef8c1cc 100644
--- a/.github/workflows/sonarqube-main.yml
+++ b/.github/workflows/sonarqube-main.yml
@@ -1,7 +1,7 @@
name: SonarCloud Full Metrics (main branch)
on:
- pull_request:
+ push:
branches: [main, master]
workflow_dispatch:
@@ -19,13 +19,19 @@ jobs:
with:
fetch-depth: 0
+ - name: Python-Version aus Dockerfile lesen
+ id: pyver
+ run: |
+ VERSION=$(grep -oP '(?<=python:)\d+\.\d+' project/Dockerfile-backend | head -1)
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+
# =====================================================
# Backend-Tests + Coverage
# =====================================================
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: "3.10"
+ python-version: ${{ steps.pyver.outputs.version }}
- name: Install backend deps
working-directory: project/backend
@@ -56,15 +62,14 @@ jobs:
-Dsonar.organization=galacticcodegambit
-Dproject.settings=sonar-project.properties
-Dsonar.python.coverage.reportPaths=backend/coverage.xml
- -Dsonar.python.version=3.10
+ -Dsonar.python.version=${{ steps.pyver.outputs.version }}
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Wait for analysis
id: qg
- uses: SonarSource/sonarqube-quality-gate-action@master
+ uses: SonarSource/sonarqube-quality-gate-action@v1.2.0
timeout-minutes: 5
- continue-on-error: true
with:
scanMetadataReportFile: project/.scannerwork/report-task.txt
env:
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 22ff9bb..e2bc9c3 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -19,13 +19,19 @@
with:
fetch-depth: 0
+ - name: Python-Version aus Dockerfile lesen
+ id: pyver
+ run: |
+ VERSION=$(grep -oP '(?<=python:)\d+\.\d+' project/Dockerfile-backend | head -1)
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+
# =====================================================
# BACKEND (Python / FastAPI) – Tests + Coverage
# =====================================================
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: "3.10"
+ python-version: ${{ steps.pyver.outputs.version }}
- name: Install backend dependencies
working-directory: project/backend
@@ -56,16 +62,15 @@
-Dsonar.organization=galacticcodegambit
-Dproject.settings=sonar-project.properties
-Dsonar.python.coverage.reportPaths=backend/coverage.xml
- -Dsonar.python.version=3.10
+ -Dsonar.python.version=${{ steps.pyver.outputs.version }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: SonarCloud Quality Gate check
id: sonar-qg
- uses: SonarSource/sonarqube-quality-gate-action@master
+ uses: SonarSource/sonarqube-quality-gate-action@v1.2.0
timeout-minutes: 5
- continue-on-error: true
with:
scanMetadataReportFile: project/.scannerwork/report-task.txt
env:
diff --git a/project/backend/Database.py b/project/backend/Database.py
index 69ad3c9..6fc2b65 100644
--- a/project/backend/Database.py
+++ b/project/backend/Database.py
@@ -275,7 +275,7 @@ def getIngridientByName(name: str):
FROM Ingredient
WHERE name = ?
""",
- (name),
+ (name,),
)
row = cur.fetchone()
return dict(row) if row else None
diff --git a/project/backend/Recipe.py b/project/backend/Recipe.py
index 5c0ca46..356012a 100644
--- a/project/backend/Recipe.py
+++ b/project/backend/Recipe.py
@@ -16,11 +16,11 @@ def __init__(self, name: str, ingredients: list[Ingredient], description: str):
def saveInDB(self) -> bool:
rid = addRecipe(self.__name, self.__description, None)
for ingridient in self.__ingredients:
- zid = getIngridientByName(ingridient.getName())['id']
- if not zid:
+ result = getIngridientByName(ingridient.getName())
+ if not result:
return False
- else:
- addIngredientToRecipe(zid, rid, ingridient.getAmount())
+ zid = result['id']
+ addIngredientToRecipe(zid, rid, ingridient.getAmount())
return True
def getName(self) -> str:
diff --git a/project/frontend/lib/auth.tsx b/project/frontend/lib/auth.tsx
index 0fc4026..470fb1b 100644
--- a/project/frontend/lib/auth.tsx
+++ b/project/frontend/lib/auth.tsx
@@ -6,7 +6,6 @@ import {
useState,
useEffect,
useCallback,
- useMemo,
type ReactNode,
} from "react";
@@ -222,7 +221,7 @@ export function AuthProvider({ children }: Readonly<{ children: ReactNode }>) {
setUser(null);
}, []);
- return (
+ return (
{children}
diff --git a/project/sonar-project.properties b/project/sonar-project.properties
index 78c8d4b..1252ae3 100644
--- a/project/sonar-project.properties
+++ b/project/sonar-project.properties
@@ -7,13 +7,13 @@ sonar.projectVersion=1.0
# Quellen + Tests
sonar.sources=backend,frontend/app
sonar.tests=backend/tests
-sonar.exclusions=**/node_modules/**,**/__pycache__/**,**/.next/**,frontend/app/components/ui/**,**/*.config.*,**/coverage/**,backend/tests/**
+sonar.exclusions=**/node_modules/**,**/__pycache__/**,**/.next/**,frontend/app/components/ui/**,**/*.config.*,**/coverage/**
# Encoding
sonar.sourceEncoding=UTF-8
# Python
-sonar.python.version=3.10
+sonar.python.version=3.11
sonar.python.coverage.reportPaths=backend/coverage.xml
# CPD-Schwellwerte auf das absolute Minimum gestellt:
From f16a1383b47dd3600ac04813e7ef9403589bacc9 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 26 May 2026 13:33:53 +0200
Subject: [PATCH 77/85] Fix: Code Preview from Copilot fixes
---
.github/workflows/sonarqube.yml | 504 +++++++++---------
.../app/profile/changePasswordPopup.tsx | 2 +-
project/frontend/lib/auth.tsx | 2 +-
3 files changed, 254 insertions(+), 254 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index e2bc9c3..340c41d 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -1,273 +1,273 @@
- name: SonarCloud Analysis
+name: SonarCloud Analysis
- on:
- pull_request:
- branches: [main, master]
- workflow_dispatch:
+on:
+ pull_request:
+ branches: [main, master]
+ workflow_dispatch:
- permissions:
- contents: read
+permissions:
+ contents: read
- jobs:
- sonarcloud:
- name: Build, Test & Sonar Scan
- runs-on: ubuntu-latest
+jobs:
+ sonarcloud:
+ name: Build, Test & Sonar Scan
+ runs-on: ubuntu-latest
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- - name: Python-Version aus Dockerfile lesen
- id: pyver
- run: |
- VERSION=$(grep -oP '(?<=python:)\d+\.\d+' project/Dockerfile-backend | head -1)
- echo "version=$VERSION" >> $GITHUB_OUTPUT
+ - name: Python-Version aus Dockerfile lesen
+ id: pyver
+ run: |
+ VERSION=$(grep -oP '(?<=python:)\d+\.\d+' project/Dockerfile-backend | head -1)
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
- # =====================================================
- # BACKEND (Python / FastAPI) – Tests + Coverage
- # =====================================================
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: ${{ steps.pyver.outputs.version }}
+ # =====================================================
+ # BACKEND (Python / FastAPI) – Tests + Coverage
+ # =====================================================
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ steps.pyver.outputs.version }}
- - name: Install backend dependencies
- working-directory: project/backend
- run: |
- python -m pip install --upgrade pip
- python -m pip install -r requirements.txt
- python -m pip install "pytest-cov>=5,<7" "coverage>=7"
+ - name: Install backend dependencies
+ working-directory: project/backend
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install -r requirements.txt
+ python -m pip install "pytest-cov>=5,<7" "coverage>=7"
- - name: Run backend tests with coverage
- working-directory: project
- run: |
- python -m pytest \
- --cov=backend \
- --cov-report=xml:backend/coverage.xml \
- --cov-report=term-missing \
- backend/tests/
- continue-on-error: true
+ - name: Run backend tests with coverage
+ working-directory: project
+ run: |
+ python -m pytest \
+ --cov=backend \
+ --cov-report=xml:backend/coverage.xml \
+ --cov-report=term-missing \
+ backend/tests/
+ continue-on-error: true
- # =====================================================
- # SONARCLOUD SCAN
- # =====================================================
- - name: SonarQube Scan
- uses: SonarSource/sonarqube-scan-action@v3
- with:
- projectBaseDir: project
- args: >
- -Dsonar.projectKey=GalacticCodeGambit_LazyCook
- -Dsonar.organization=galacticcodegambit
- -Dproject.settings=sonar-project.properties
- -Dsonar.python.coverage.reportPaths=backend/coverage.xml
- -Dsonar.python.version=${{ steps.pyver.outputs.version }}
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ # =====================================================
+ # SONARCLOUD SCAN
+ # =====================================================
+ - name: SonarQube Scan
+ uses: SonarSource/sonarqube-scan-action@v3
+ with:
+ projectBaseDir: project
+ args: >
+ -Dsonar.projectKey=GalacticCodeGambit_LazyCook
+ -Dsonar.organization=galacticcodegambit
+ -Dproject.settings=sonar-project.properties
+ -Dsonar.python.coverage.reportPaths=backend/coverage.xml
+ -Dsonar.python.version=${{ steps.pyver.outputs.version }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- - name: SonarCloud Quality Gate check
- id: sonar-qg
- uses: SonarSource/sonarqube-quality-gate-action@v1.2.0
- timeout-minutes: 5
- with:
- scanMetadataReportFile: project/.scannerwork/report-task.txt
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ - name: SonarCloud Quality Gate check
+ id: sonar-qg
+ uses: SonarSource/sonarqube-quality-gate-action@v1.2.0
+ timeout-minutes: 5
+ with:
+ scanMetadataReportFile: project/.scannerwork/report-task.txt
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- # =====================================================
- # METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
- # =====================================================
- - name: Render SonarCloud metrics to Job Summary
- if: always()
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- SONAR_HOST: https://sonarcloud.io
- QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
- PR_NUMBER: ${{ github.event.pull_request.number }}
- run: |
- set +e
-
- PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
- | head -1 | cut -d'=' -f2 | tr -d ' \r\n')
-
- # PR-Kontext erkennen: dann Daten der PR-Analyse holen, nicht der Main-Branch
- PR_PARAM=""
- if [ -n "${PR_NUMBER}" ]; then
- PR_PARAM="&pullRequest=${PR_NUMBER}"
- echo "PR-Modus aktiv – Daten werden für PR #${PR_NUMBER} geholt"
- else
- echo "Branch-Modus aktiv – Daten werden für den Default-Branch geholt"
- fi
-
- METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
-
- API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}${PR_PARAM}"
- HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
- -u "${SONAR_TOKEN}:" "${API_URL}")
- RESP=$(cat /tmp/sonar.json)
+ # =====================================================
+ # METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
+ # =====================================================
+ - name: Render SonarCloud metrics to Job Summary
+ if: always()
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_HOST: https://sonarcloud.io
+ QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ run: |
+ set +e
+
+ PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
+ | head -1 | cut -d'=' -f2 | tr -d ' \r\n')
+
+ # PR-Kontext erkennen: dann Daten der PR-Analyse holen, nicht der Main-Branch
+ PR_PARAM=""
+ if [ -n "${PR_NUMBER}" ]; then
+ PR_PARAM="&pullRequest=${PR_NUMBER}"
+ echo "PR-Modus aktiv – Daten werden für PR #${PR_NUMBER} geholt"
+ else
+ echo "Branch-Modus aktiv – Daten werden für den Default-Branch geholt"
+ fi
+
+ METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
+
+ API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}${PR_PARAM}"
+ HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
+ -u "${SONAR_TOKEN}:" "${API_URL}")
+ RESP=$(cat /tmp/sonar.json)
- HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
-
- # Zusätzlich Branch-Daten holen für Metriken, die im PR-Modus leer sind
- # (z.B. ncloc, complexity, cognitive_complexity – Projekt-strukturelle Werte)
- BRANCH_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}"
- curl -s -o /tmp/sonar_branch.json -u "${SONAR_TOKEN}:" "${BRANCH_URL}" >/dev/null
- BRANCH_RESP=$(cat /tmp/sonar_branch.json)
-
- SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
-
- if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
- {
- echo "## SonarCloud Analyse"
- echo ""
- echo ":warning: Konnte die Metriken nicht abrufen."
- echo ""
- echo "- **HTTP Status:** ${HTTP_CODE}"
- echo "- **Project Key:** ${PROJECT_KEY}"
- echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
- } >> "$SUMMARY_FILE"
- exit 0
- fi
-
- # Holt Metrik primär aus PR-Response, fällt auf Branch-Response zurück
- get() {
- local v
- v=$(echo "${RESP}" | jq -r --arg k "$1" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end')
- if [ "$v" = "-" ] || [ -z "$v" ]; then
- v=$(echo "${BRANCH_RESP}" | jq -r --arg k "$1" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end')
- fi
- echo "$v"
- }
-
- # Holt Metrik für Sub-Komponente.
- # SonarCloud-Sub-Komponente kann ':' nicht immer per
- # measures/component-Endpoint adressieren – wir nutzen component_tree
- # mit dem 'q'-Filter, der nach Pfad-Suffix sucht.
- get_sub() {
- local path="$1"
- local metric="$2"
- local tree_url tree_resp v
+ HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
- # Branch-Tree erstmal, danach PR falls vorhanden – beide pruefen
- for try_pr in branch pr; do
- tree_url="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=${metric}&qualifiers=DIR&q=${path}&ps=20"
- if [ "$try_pr" = "pr" ] && [ -n "${PR_NUMBER}" ]; then
- tree_url="${tree_url}&pullRequest=${PR_NUMBER}"
- elif [ "$try_pr" = "pr" ]; then
- continue
- fi
- tree_resp=$(curl -s -u "${SONAR_TOKEN}:" "${tree_url}")
- # Sucht in den Components die mit path enden ODER deren path == argument
- v=$(echo "${tree_resp}" | jq -r --arg p "$path" --arg k "$metric" \
- '[.components[]? | select(.path == $p)]
- | .[0].measures[]? | select(.metric==$k) | .value' 2>/dev/null)
- if [ -n "$v" ] && [ "$v" != "null" ] && [ "$v" != "-" ]; then
- echo "$v"
- return
- fi
- done
- echo "-"
- }
-
- rating() {
- case "$(get "$1")" in
- 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
- 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
- esac
- }
-
- DEBT_MIN=$(get sqale_index)
- if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
- DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
- DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
- else
- DEBT_FMT="-"
- fi
-
- if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
- QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
- else
- QG_ICON="FAILED"; QG_EMOJI=":x:"
- fi
-
+ # Zusätzlich Branch-Daten holen für Metriken, die im PR-Modus leer sind
+ # (z.B. ncloc, complexity, cognitive_complexity – Projekt-strukturelle Werte)
+ BRANCH_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}"
+ curl -s -o /tmp/sonar_branch.json -u "${SONAR_TOKEN}:" "${BRANCH_URL}" >/dev/null
+ BRANCH_RESP=$(cat /tmp/sonar_branch.json)
+
+ SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
+
+ if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
{
- echo "## SonarCloud Analyse – LazyCook"
- echo ""
- echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
- echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
- echo ""
- echo "### Coverage"
+ echo "## SonarCloud Analyse"
echo ""
- echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
- echo "|---|---|---|---|---|"
- echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
- echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
- echo ""
-
- echo "### Duplikate & Komplexität"
- echo ""
- echo "| Bereich | Metrik | Wert |"
- echo "|---|---|---|"
- echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
- echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
- echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
- echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
- echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
- echo ""
-
- echo "### Alle Dateien mit Duplikaten"
+ echo ":warning: Konnte die Metriken nicht abrufen."
echo ""
+ echo "- **HTTP Status:** ${HTTP_CODE}"
+ echo "- **Project Key:** ${PROJECT_KEY}"
+ echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
} >> "$SUMMARY_FILE"
-
- TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false${PR_PARAM}"
- TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
-
- FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
-
- {
- if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
- echo "_Keine Datei hat aktuell Duplikate._"
- echo ""
- else
- echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
- echo ""
- echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
- echo "|---|---:|---:|---:|---:|"
- echo "${TREE_RESP}" | jq -r '
- .components[]?
- | (.path // .key) as $p
- | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
- | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
- | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
- | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
- | select(($b | tonumber) > 0)
- | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
- '
- echo ""
+ exit 0
+ fi
+
+ # Holt Metrik primär aus PR-Response, fällt auf Branch-Response zurück
+ get() {
+ local v
+ v=$(echo "${RESP}" | jq -r --arg k "$1" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end')
+ if [ "$v" = "-" ] || [ -z "$v" ]; then
+ v=$(echo "${BRANCH_RESP}" | jq -r --arg k "$1" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end')
+ fi
+ echo "$v"
+ }
+
+ # Holt Metrik für Sub-Komponente.
+ # SonarCloud-Sub-Komponente kann ':' nicht immer per
+ # measures/component-Endpoint adressieren – wir nutzen component_tree
+ # mit dem 'q'-Filter, der nach Pfad-Suffix sucht.
+ get_sub() {
+ local path="$1"
+ local metric="$2"
+ local tree_url tree_resp v
+
+ # Branch-Tree erstmal, danach PR falls vorhanden – beide pruefen
+ for try_pr in branch pr; do
+ tree_url="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=${metric}&qualifiers=DIR&q=${path}&ps=20"
+ if [ "$try_pr" = "pr" ] && [ -n "${PR_NUMBER}" ]; then
+ tree_url="${tree_url}&pullRequest=${PR_NUMBER}"
+ elif [ "$try_pr" = "pr" ]; then
+ continue
fi
- echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
+ tree_resp=$(curl -s -u "${SONAR_TOKEN}:" "${tree_url}")
+ # Sucht in den Components die mit path enden ODER deren path == argument
+ v=$(echo "${tree_resp}" | jq -r --arg p "$path" --arg k "$metric" \
+ '[.components[]? | select(.path == $p)]
+ | .[0].measures[]? | select(.metric==$k) | .value' 2>/dev/null)
+ if [ -n "$v" ] && [ "$v" != "null" ] && [ "$v" != "-" ]; then
+ echo "$v"
+ return
+ fi
+ done
+ echo "-"
+ }
+
+ rating() {
+ case "$(get "$1")" in
+ 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
+ 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
+ esac
+ }
+
+ DEBT_MIN=$(get sqale_index)
+ if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
+ DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
+ DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
+ else
+ DEBT_FMT="-"
+ fi
+
+ if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
+ QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
+ else
+ QG_ICON="FAILED"; QG_EMOJI=":x:"
+ fi
+
+ {
+ echo "## SonarCloud Analyse – LazyCook"
+ echo ""
+ echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
+ echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
+ echo ""
+ echo "### Coverage"
+ echo ""
+ echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
+ echo "|---|---|---|---|---|"
+ echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
+ echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
+ echo ""
+
+ echo "### Duplikate & Komplexität"
+ echo ""
+ echo "| Bereich | Metrik | Wert |"
+ echo "|---|---|---|"
+ echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
+ echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
+ echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
+ echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
+ echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
+ echo ""
+
+ echo "### Alle Dateien mit Duplikaten"
+ echo ""
+ } >> "$SUMMARY_FILE"
+
+ TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false${PR_PARAM}"
+ TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
+
+ FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
+
+ {
+ if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
+ echo "_Keine Datei hat aktuell Duplikate._"
echo ""
-
- echo "### Ratings & Issues"
+ else
+ echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
echo ""
- echo "| Bewertung | Wert | Anzahl |"
- echo "|---|---|---|"
- echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
- echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
- echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
- echo "| Technische Schuld | ${DEBT_FMT} | – |"
- } >> "$SUMMARY_FILE"
+ echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
+ echo "|---|---:|---:|---:|---:|"
+ echo "${TREE_RESP}" | jq -r '
+ .components[]?
+ | (.path // .key) as $p
+ | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
+ | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
+ | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
+ | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
+ | select(($b | tonumber) > 0)
+ | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
+ '
+ echo ""
+ fi
+ echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
+ echo ""
+
+ echo "### Ratings & Issues"
+ echo ""
+ echo "| Bewertung | Wert | Anzahl |"
+ echo "|---|---|---|"
+ echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
+ echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
+ echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
+ echo "| Technische Schuld | ${DEBT_FMT} | – |"
+ } >> "$SUMMARY_FILE"
- - name: Fail job on Quality Gate failure
- if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
- run: |
- echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
- exit 1
\ No newline at end of file
+ - name: Fail job on Quality Gate failure
+ if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
+ run: |
+ echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
+ exit 1
\ No newline at end of file
diff --git a/project/frontend/app/profile/changePasswordPopup.tsx b/project/frontend/app/profile/changePasswordPopup.tsx
index 12423c8..35344c5 100644
--- a/project/frontend/app/profile/changePasswordPopup.tsx
+++ b/project/frontend/app/profile/changePasswordPopup.tsx
@@ -7,7 +7,7 @@ import Field from "@/app/components/fields";
function PasswordInput({ value, onChange, placeholder, onKeyDown, ariaLabel }: {
readonly value: string;
- readonly onChange: (v: string) => void;
+ readonly onChange: (v: string) => void;
readonly placeholder: string;
readonly onKeyDown?: (e: React.KeyboardEvent) => void;
readonly ariaLabel?: string;
diff --git a/project/frontend/lib/auth.tsx b/project/frontend/lib/auth.tsx
index 470fb1b..fcf4b49 100644
--- a/project/frontend/lib/auth.tsx
+++ b/project/frontend/lib/auth.tsx
@@ -108,7 +108,7 @@ async function fetchWithAuth(url: string, options: RequestInit = {}): Promise
Date: Tue, 26 May 2026 13:42:19 +0200
Subject: [PATCH 78/85] Fix: Code Preview from Copilot fixes
---
.github/workflows/sonarqube-main.yml | 1 +
.github/workflows/sonarqube.yml | 498 +++++++++++++--------------
2 files changed, 246 insertions(+), 253 deletions(-)
diff --git a/.github/workflows/sonarqube-main.yml b/.github/workflows/sonarqube-main.yml
index ef8c1cc..2ed3abc 100644
--- a/.github/workflows/sonarqube-main.yml
+++ b/.github/workflows/sonarqube-main.yml
@@ -70,6 +70,7 @@ jobs:
id: qg
uses: SonarSource/sonarqube-quality-gate-action@v1.2.0
timeout-minutes: 5
+ continue-on-error: true
with:
scanMetadataReportFile: project/.scannerwork/report-task.txt
env:
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index e2bc9c3..faa0535 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -1,273 +1,265 @@
- name: SonarCloud Analysis
+name: SonarCloud Analysis
- on:
- pull_request:
- branches: [main, master]
- workflow_dispatch:
+on:
+ pull_request:
+ branches: [main, master]
+ workflow_dispatch:
- permissions:
- contents: read
+permissions:
+ contents: read
- jobs:
- sonarcloud:
- name: Build, Test & Sonar Scan
- runs-on: ubuntu-latest
+jobs:
+ sonarcloud:
+ name: Build, Test & Sonar Scan
+ runs-on: ubuntu-latest
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- - name: Python-Version aus Dockerfile lesen
- id: pyver
- run: |
- VERSION=$(grep -oP '(?<=python:)\d+\.\d+' project/Dockerfile-backend | head -1)
- echo "version=$VERSION" >> $GITHUB_OUTPUT
+ - name: Python-Version aus Dockerfile lesen
+ id: pyver
+ run: |
+ VERSION=$(grep -oP '(?<=python:)\d+\.\d+' project/Dockerfile-backend | head -1)
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
- # =====================================================
- # BACKEND (Python / FastAPI) – Tests + Coverage
- # =====================================================
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: ${{ steps.pyver.outputs.version }}
+ # =====================================================
+ # BACKEND (Python / FastAPI) – Tests + Coverage
+ # =====================================================
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ steps.pyver.outputs.version }}
- - name: Install backend dependencies
- working-directory: project/backend
- run: |
- python -m pip install --upgrade pip
- python -m pip install -r requirements.txt
- python -m pip install "pytest-cov>=5,<7" "coverage>=7"
+ - name: Install backend dependencies
+ working-directory: project/backend
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install -r requirements.txt
+ python -m pip install "pytest-cov>=5,<7" "coverage>=7"
- - name: Run backend tests with coverage
- working-directory: project
- run: |
- python -m pytest \
- --cov=backend \
- --cov-report=xml:backend/coverage.xml \
- --cov-report=term-missing \
- backend/tests/
- continue-on-error: true
+ - name: Run backend tests with coverage
+ working-directory: project
+ run: |
+ python -m pytest \
+ --cov=backend \
+ --cov-report=xml:backend/coverage.xml \
+ --cov-report=term-missing \
+ backend/tests/
+ continue-on-error: true
- # =====================================================
- # SONARCLOUD SCAN
- # =====================================================
- - name: SonarQube Scan
- uses: SonarSource/sonarqube-scan-action@v3
- with:
- projectBaseDir: project
- args: >
- -Dsonar.projectKey=GalacticCodeGambit_LazyCook
- -Dsonar.organization=galacticcodegambit
- -Dproject.settings=sonar-project.properties
- -Dsonar.python.coverage.reportPaths=backend/coverage.xml
- -Dsonar.python.version=${{ steps.pyver.outputs.version }}
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ # =====================================================
+ # SONARCLOUD SCAN
+ # =====================================================
+ - name: SonarQube Scan
+ uses: SonarSource/sonarqube-scan-action@v3
+ with:
+ projectBaseDir: project
+ args: >
+ -Dsonar.projectKey=GalacticCodeGambit_LazyCook
+ -Dsonar.organization=galacticcodegambit
+ -Dproject.settings=sonar-project.properties
+ -Dsonar.python.coverage.reportPaths=backend/coverage.xml
+ -Dsonar.python.version=${{ steps.pyver.outputs.version }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- - name: SonarCloud Quality Gate check
- id: sonar-qg
- uses: SonarSource/sonarqube-quality-gate-action@v1.2.0
- timeout-minutes: 5
- with:
- scanMetadataReportFile: project/.scannerwork/report-task.txt
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ - name: SonarCloud Quality Gate check
+ id: sonar-qg
+ uses: SonarSource/sonarqube-quality-gate-action@v1.2.0
+ timeout-minutes: 5
+ continue-on-error: true
+ with:
+ scanMetadataReportFile: project/.scannerwork/report-task.txt
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- # =====================================================
- # METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
- # =====================================================
- - name: Render SonarCloud metrics to Job Summary
- if: always()
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- SONAR_HOST: https://sonarcloud.io
- QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
- PR_NUMBER: ${{ github.event.pull_request.number }}
- run: |
- set +e
-
- PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
- | head -1 | cut -d'=' -f2 | tr -d ' \r\n')
-
- # PR-Kontext erkennen: dann Daten der PR-Analyse holen, nicht der Main-Branch
- PR_PARAM=""
- if [ -n "${PR_NUMBER}" ]; then
- PR_PARAM="&pullRequest=${PR_NUMBER}"
- echo "PR-Modus aktiv – Daten werden für PR #${PR_NUMBER} geholt"
- else
- echo "Branch-Modus aktiv – Daten werden für den Default-Branch geholt"
- fi
-
- METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
-
- API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}${PR_PARAM}"
- HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
- -u "${SONAR_TOKEN}:" "${API_URL}")
- RESP=$(cat /tmp/sonar.json)
+ # =====================================================
+ # METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
+ # =====================================================
+ - name: Render SonarCloud metrics to Job Summary
+ if: always()
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_HOST: https://sonarcloud.io
+ QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ run: |
+ set +e
+
+ PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
+ | head -1 | cut -d'=' -f2 | tr -d ' \r\n')
+
+ # PR-Kontext erkennen: dann Daten der PR-Analyse holen, nicht der Main-Branch
+ PR_PARAM=""
+ if [ -n "${PR_NUMBER}" ]; then
+ PR_PARAM="&pullRequest=${PR_NUMBER}"
+ echo "PR-Modus aktiv – Daten werden für PR #${PR_NUMBER} geholt"
+ else
+ echo "Branch-Modus aktiv – Daten werden für den Default-Branch geholt"
+ fi
+
+ METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
+
+ API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}${PR_PARAM}"
+ HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
+ -u "${SONAR_TOKEN}:" "${API_URL}")
+ RESP=$(cat /tmp/sonar.json)
- HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
-
- # Zusätzlich Branch-Daten holen für Metriken, die im PR-Modus leer sind
- # (z.B. ncloc, complexity, cognitive_complexity – Projekt-strukturelle Werte)
- BRANCH_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}"
- curl -s -o /tmp/sonar_branch.json -u "${SONAR_TOKEN}:" "${BRANCH_URL}" >/dev/null
- BRANCH_RESP=$(cat /tmp/sonar_branch.json)
-
- SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
-
- if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
- {
- echo "## SonarCloud Analyse"
- echo ""
- echo ":warning: Konnte die Metriken nicht abrufen."
- echo ""
- echo "- **HTTP Status:** ${HTTP_CODE}"
- echo "- **Project Key:** ${PROJECT_KEY}"
- echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
- } >> "$SUMMARY_FILE"
- exit 0
- fi
-
- # Holt Metrik primär aus PR-Response, fällt auf Branch-Response zurück
- get() {
- local v
- v=$(echo "${RESP}" | jq -r --arg k "$1" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end')
- if [ "$v" = "-" ] || [ -z "$v" ]; then
- v=$(echo "${BRANCH_RESP}" | jq -r --arg k "$1" \
- 'if (.component? and .component.measures?) then
- ((.component.measures[] | select(.metric==$k) | .value) // "-")
- else "-" end')
- fi
- echo "$v"
- }
-
- # Holt Metrik für Sub-Komponente.
- # SonarCloud-Sub-Komponente kann ':' nicht immer per
- # measures/component-Endpoint adressieren – wir nutzen component_tree
- # mit dem 'q'-Filter, der nach Pfad-Suffix sucht.
- get_sub() {
- local path="$1"
- local metric="$2"
- local tree_url tree_resp v
+ HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
- # Branch-Tree erstmal, danach PR falls vorhanden – beide pruefen
- for try_pr in branch pr; do
- tree_url="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=${metric}&qualifiers=DIR&q=${path}&ps=20"
- if [ "$try_pr" = "pr" ] && [ -n "${PR_NUMBER}" ]; then
- tree_url="${tree_url}&pullRequest=${PR_NUMBER}"
- elif [ "$try_pr" = "pr" ]; then
- continue
- fi
- tree_resp=$(curl -s -u "${SONAR_TOKEN}:" "${tree_url}")
- # Sucht in den Components die mit path enden ODER deren path == argument
- v=$(echo "${tree_resp}" | jq -r --arg p "$path" --arg k "$metric" \
- '[.components[]? | select(.path == $p)]
- | .[0].measures[]? | select(.metric==$k) | .value' 2>/dev/null)
- if [ -n "$v" ] && [ "$v" != "null" ] && [ "$v" != "-" ]; then
- echo "$v"
- return
- fi
- done
- echo "-"
- }
-
- rating() {
- case "$(get "$1")" in
- 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
- 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
- esac
- }
-
- DEBT_MIN=$(get sqale_index)
- if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
- DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
- DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
- else
- DEBT_FMT="-"
- fi
-
- if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
- QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
- else
- QG_ICON="FAILED"; QG_EMOJI=":x:"
- fi
-
+ # Zusätzlich Branch-Daten holen für Metriken, die im PR-Modus leer sind
+ # (z.B. ncloc, complexity, cognitive_complexity – Projekt-strukturelle Werte)
+ BRANCH_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}"
+ curl -s -o /tmp/sonar_branch.json -u "${SONAR_TOKEN}:" "${BRANCH_URL}" >/dev/null
+ BRANCH_RESP=$(cat /tmp/sonar_branch.json)
+
+ SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
+
+ if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
{
- echo "## SonarCloud Analyse – LazyCook"
- echo ""
- echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
- echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
- echo ""
- echo "### Coverage"
- echo ""
- echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
- echo "|---|---|---|---|---|"
- echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
- echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
+ echo "## SonarCloud Analyse"
echo ""
-
- echo "### Duplikate & Komplexität"
- echo ""
- echo "| Bereich | Metrik | Wert |"
- echo "|---|---|---|"
- echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
- echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
- echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
- echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
- echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
- echo ""
-
- echo "### Alle Dateien mit Duplikaten"
+ echo ":warning: Konnte die Metriken nicht abrufen."
echo ""
+ echo "- **HTTP Status:** ${HTTP_CODE}"
+ echo "- **Project Key:** ${PROJECT_KEY}"
+ echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
} >> "$SUMMARY_FILE"
-
- TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false${PR_PARAM}"
- TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
-
- FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
-
- {
- if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
- echo "_Keine Datei hat aktuell Duplikate._"
- echo ""
- else
- echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
- echo ""
- echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
- echo "|---|---:|---:|---:|---:|"
- echo "${TREE_RESP}" | jq -r '
- .components[]?
- | (.path // .key) as $p
- | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
- | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
- | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
- | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
- | select(($b | tonumber) > 0)
- | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
- '
- echo ""
+ exit 0
+ fi
+
+ # Holt Metrik primär aus PR-Response, fällt auf Branch-Response zurück
+ get() {
+ local v
+ v=$(echo "${RESP}" | jq -r --arg k "$1" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end')
+ if [ "$v" = "-" ] || [ -z "$v" ]; then
+ v=$(echo "${BRANCH_RESP}" | jq -r --arg k "$1" \
+ 'if (.component? and .component.measures?) then
+ ((.component.measures[] | select(.metric==$k) | .value) // "-")
+ else "-" end')
+ fi
+ echo "$v"
+ }
+
+ # Holt Metrik für Sub-Komponente.
+ # SonarCloud-Sub-Komponente kann ':' nicht immer per
+ # measures/component-Endpoint adressieren – wir nutzen component_tree
+ # mit dem 'q'-Filter, der nach Pfad-Suffix sucht.
+ get_sub() {
+ local path="$1"
+ local metric="$2"
+ local tree_url tree_resp v
+
+ # Branch-Tree erstmal, danach PR falls vorhanden – beide pruefen
+ for try_pr in branch pr; do
+ tree_url="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=${metric}&qualifiers=DIR&q=${path}&ps=20"
+ if [ "$try_pr" = "pr" ] && [ -n "${PR_NUMBER}" ]; then
+ tree_url="${tree_url}&pullRequest=${PR_NUMBER}"
+ elif [ "$try_pr" = "pr" ]; then
+ continue
+ fi
+ tree_resp=$(curl -s -u "${SONAR_TOKEN}:" "${tree_url}")
+ # Sucht in den Components die mit path enden ODER deren path == argument
+ v=$(echo "${tree_resp}" | jq -r --arg p "$path" --arg k "$metric" \
+ '[.components[]? | select(.path == $p)]
+ | .[0].measures[]? | select(.metric==$k) | .value' 2>/dev/null)
+ if [ -n "$v" ] && [ "$v" != "null" ] && [ "$v" != "-" ]; then
+ echo "$v"
+ return
fi
- echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
+ done
+ echo "-"
+ }
+
+ rating() {
+ case "$(get "$1")" in
+ 1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
+ 4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
+ esac
+ }
+
+ DEBT_MIN=$(get sqale_index)
+ if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
+ DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
+ DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
+ else
+ DEBT_FMT="-"
+ fi
+
+ if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
+ QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
+ else
+ QG_ICON="FAILED"; QG_EMOJI=":x:"
+ fi
+
+ {
+ echo "## SonarCloud Analyse – LazyCook"
+ echo ""
+ echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
+ echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
+ echo ""
+ echo "### Coverage"
+ echo ""
+ echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
+ echo "|---|---|---|---|---|"
+ echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
+ echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
+ echo ""
+
+ echo "### Duplikate & Komplexität"
+ echo ""
+ echo "| Bereich | Metrik | Wert |"
+ echo "|---|---|---|"
+ echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
+ echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
+ echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
+ echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
+ echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
+ echo ""
+
+ echo "### Alle Dateien mit Duplikaten"
+ echo ""
+ } >> "$SUMMARY_FILE"
+
+ TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false${PR_PARAM}"
+ TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
+
+ FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
+
+ {
+ if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
+ echo "_Keine Datei hat aktuell Duplikate._"
echo ""
-
- echo "### Ratings & Issues"
+ else
+ echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
echo ""
- echo "| Bewertung | Wert | Anzahl |"
- echo "|---|---|---|"
- echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
- echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
- echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
- echo "| Technische Schuld | ${DEBT_FMT} | – |"
- } >> "$SUMMARY_FILE"
-
- - name: Fail job on Quality Gate failure
- if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
- run: |
- echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
- exit 1
\ No newline at end of file
+ echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
+ echo "|---|---:|---:|---:|---:|"
+ echo "${TREE_RESP}" | jq -r '
+ .components[]?
+ | (.path // .key) as $p
+ | ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
+ | ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
+ | ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
+ | ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
+ | select(($b | tonumber) > 0)
+ | "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
+ '
+ echo ""
+ fi
+ echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
+ echo ""
+
+ echo "### Ratings & Issues"
+ echo ""
+ echo "| Bewertung | Wert | Anzahl |"
+ echo "|---|---|---|"
+ echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
+ echo "| Security Rating | $(rat
\ No newline at end of file
From 578ee1f54d75b49744992d8221fb108491ca8114 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 26 May 2026 13:58:32 +0200
Subject: [PATCH 79/85] Fix: Code Preview from Copilot fixes
---
.github/workflows/sonarqube-main.yml | 10 ++--------
.github/workflows/sonarqube.yml | 10 ++--------
2 files changed, 4 insertions(+), 16 deletions(-)
diff --git a/.github/workflows/sonarqube-main.yml b/.github/workflows/sonarqube-main.yml
index 2ed3abc..7586418 100644
--- a/.github/workflows/sonarqube-main.yml
+++ b/.github/workflows/sonarqube-main.yml
@@ -19,19 +19,13 @@ jobs:
with:
fetch-depth: 0
- - name: Python-Version aus Dockerfile lesen
- id: pyver
- run: |
- VERSION=$(grep -oP '(?<=python:)\d+\.\d+' project/Dockerfile-backend | head -1)
- echo "version=$VERSION" >> $GITHUB_OUTPUT
-
# =====================================================
# Backend-Tests + Coverage
# =====================================================
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: ${{ steps.pyver.outputs.version }}
+ python-version: "3.11"
- name: Install backend deps
working-directory: project/backend
@@ -62,7 +56,7 @@ jobs:
-Dsonar.organization=galacticcodegambit
-Dproject.settings=sonar-project.properties
-Dsonar.python.coverage.reportPaths=backend/coverage.xml
- -Dsonar.python.version=${{ steps.pyver.outputs.version }}
+ -Dsonar.python.version=3.11
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index faa0535..ee713c4 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -19,19 +19,13 @@ jobs:
with:
fetch-depth: 0
- - name: Python-Version aus Dockerfile lesen
- id: pyver
- run: |
- VERSION=$(grep -oP '(?<=python:)\d+\.\d+' project/Dockerfile-backend | head -1)
- echo "version=$VERSION" >> $GITHUB_OUTPUT
-
# =====================================================
# BACKEND (Python / FastAPI) – Tests + Coverage
# =====================================================
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: ${{ steps.pyver.outputs.version }}
+ python-version: "3.11"
- name: Install backend dependencies
working-directory: project/backend
@@ -62,7 +56,7 @@ jobs:
-Dsonar.organization=galacticcodegambit
-Dproject.settings=sonar-project.properties
-Dsonar.python.coverage.reportPaths=backend/coverage.xml
- -Dsonar.python.version=${{ steps.pyver.outputs.version }}
+ -Dsonar.python.version=3.11
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
From b54f9dc8d151e1ed8f3513db68e6ba7ef5de657b Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 26 May 2026 14:04:37 +0200
Subject: [PATCH 80/85] Fix: Code Preview from Copilot fixes
---
.github/workflows/sonarqube-main.yml | 2 +-
.github/workflows/sonarqube.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/sonarqube-main.yml b/.github/workflows/sonarqube-main.yml
index 7586418..ab393ec 100644
--- a/.github/workflows/sonarqube-main.yml
+++ b/.github/workflows/sonarqube-main.yml
@@ -62,7 +62,7 @@ jobs:
- name: Wait for analysis
id: qg
- uses: SonarSource/sonarqube-quality-gate-action@v1.2.0
+ uses: SonarSource/sonarqube-quality-gate-action@master
timeout-minutes: 5
continue-on-error: true
with:
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index ee713c4..f581e9d 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -63,7 +63,7 @@ jobs:
- name: SonarCloud Quality Gate check
id: sonar-qg
- uses: SonarSource/sonarqube-quality-gate-action@v1.2.0
+ uses: SonarSource/sonarqube-quality-gate-action@master
timeout-minutes: 5
continue-on-error: true
with:
From dad8a699ad12ec98fdcbd456b92af8f12c481341 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 26 May 2026 14:13:13 +0200
Subject: [PATCH 81/85] Fix: Code Preview from Copilot fixes
---
.github/workflows/sonarqube.yml | 17 ++++++++++++-----
1 file changed, 12 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index f581e9d..1934586 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -25,7 +25,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.10"
- name: Install backend dependencies
working-directory: project/backend
@@ -33,7 +33,6 @@ jobs:
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install "pytest-cov>=5,<7" "coverage>=7"
-
- name: Run backend tests with coverage
working-directory: project
run: |
@@ -56,7 +55,7 @@ jobs:
-Dsonar.organization=galacticcodegambit
-Dproject.settings=sonar-project.properties
-Dsonar.python.coverage.reportPaths=backend/coverage.xml
- -Dsonar.python.version=3.11
+ -Dsonar.python.version=3.10
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
@@ -86,7 +85,7 @@ jobs:
PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
| head -1 | cut -d'=' -f2 | tr -d ' \r\n')
-
+
# PR-Kontext erkennen: dann Daten der PR-Analyse holen, nicht der Main-Branch
PR_PARAM=""
if [ -n "${PR_NUMBER}" ]; then
@@ -256,4 +255,12 @@ jobs:
echo "| Bewertung | Wert | Anzahl |"
echo "|---|---|---|"
echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
- echo "| Security Rating | $(rat
\ No newline at end of file
+ echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
+ echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
+ echo "| Technische Schuld | ${DEBT_FMT} | – |"
+ } >> "$SUMMARY_FILE"
+ - name: Fail job on Quality Gate failure
+ if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
+ run: |
+ echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
+ exit 1
\ No newline at end of file
From 6d0cd5432ff379e4b6e179793122b62ff618fdfa Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 26 May 2026 14:16:14 +0200
Subject: [PATCH 82/85] Fix: Code Preview from Copilot fixes
---
.github/workflows/sonarqube.yml | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 1934586..5b24b8a 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -260,7 +260,11 @@ jobs:
echo "| Technische Schuld | ${DEBT_FMT} | – |"
} >> "$SUMMARY_FILE"
- name: Fail job on Quality Gate failure
- if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
+ if: always()
+ env:
+ QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
run: |
- echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
- exit 1
\ No newline at end of file
+ if [ "${QG_STATUS}" != "PASSED" ]; then
+ echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
+ exit 1
+ fi
From 7180aa41acf088312b8f95aa57c92cf2d70f1169 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 26 May 2026 14:23:27 +0200
Subject: [PATCH 83/85] Fix: Code Preview from Copilot fixes
---
.github/workflows/sonarqube-main.yml | 40 +++++++++-------------------
.github/workflows/sonarqube.yml | 1 +
2 files changed, 14 insertions(+), 27 deletions(-)
diff --git a/.github/workflows/sonarqube-main.yml b/.github/workflows/sonarqube-main.yml
index ab393ec..d24aee3 100644
--- a/.github/workflows/sonarqube-main.yml
+++ b/.github/workflows/sonarqube-main.yml
@@ -25,7 +25,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.10"
- name: Install backend deps
working-directory: project/backend
@@ -33,7 +33,6 @@ jobs:
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install "pytest-cov>=5,<7" "coverage>=7"
-
- name: Run backend tests with coverage
working-directory: project
run: |
@@ -44,7 +43,7 @@ jobs:
backend/tests/
continue-on-error: true
- # =====================================================
+ # =====================================================
# SonarCloud Scan
# =====================================================
- name: SonarQube Scan
@@ -56,7 +55,7 @@ jobs:
-Dsonar.organization=galacticcodegambit
-Dproject.settings=sonar-project.properties
-Dsonar.python.coverage.reportPaths=backend/coverage.xml
- -Dsonar.python.version=3.11
+ -Dsonar.python.version=3.10
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
@@ -83,25 +82,20 @@ jobs:
set +e
KEY="GalacticCodeGambit_LazyCook"
OUT="$GITHUB_STEP_SUMMARY"
-
# Alle relevanten Branch-Metriken auf einen Schwung holen
METRICS="alert_status,bugs,vulnerabilities,code_smells,security_hotspots,security_hotspots_reviewed,coverage,line_coverage,branch_coverage,lines_to_cover,uncovered_lines,duplicated_lines_density,duplicated_blocks,duplicated_files,duplicated_lines,complexity,cognitive_complexity,classes,functions,statements,files,ncloc,comment_lines,comment_lines_density,sqale_rating,reliability_rating,security_rating,security_review_rating,sqale_index,sqale_debt_ratio,reliability_remediation_effort,security_remediation_effort"
-
URL="${SONAR_HOST}/api/measures/component?component=${KEY}&metricKeys=${METRICS}"
RESP=$(curl -s -u "${SONAR_TOKEN}:" "${URL}")
-
# Mini-Diagnose im Log
echo "Anfrage: ${URL}"
echo "Response (erste 300 Zeichen):"
echo "${RESP}" | head -c 300
echo ""
-
# Hilfsfunktion: Metrik-Wert holen, "-" als Fallback
get() {
echo "${RESP}" | jq -r --arg k "$1" \
'(.component.measures[]? | select(.metric==$k) | .value) // "-"' 2>/dev/null
}
-
# Rating-Buchstabe (1.0..5.0 → A..E)
rating() {
case "$(get "$1")" in
@@ -109,7 +103,6 @@ jobs:
4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
esac
}
-
# Minuten → Stunden + Minuten
fmt_debt() {
local m=$1
@@ -120,18 +113,15 @@ jobs:
echo "-"
fi
}
-
# Quality-Gate-Icon
if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
QG_ICON=":white_check_mark:"; QG_TXT="PASSED"
else
QG_ICON=":x:"; QG_TXT="FAILED"
fi
-
DEBT_FMT=$(fmt_debt "$(get sqale_index)")
REL_EFF_FMT=$(fmt_debt "$(get reliability_remediation_effort)")
SEC_EFF_FMT=$(fmt_debt "$(get security_remediation_effort)")
-
# ===== Backend-Coverage direkt aus coverage.xml =====
COV_FILE="project/backend/coverage.xml"
if [ -f "${COV_FILE}" ]; then
@@ -160,7 +150,6 @@ jobs:
LINES_VALID="-"; LINES_COVERED="-"; BRANCHES_VALID="-"; BRANCHES_COVERED="-"
UNCOVERED_LINES="-"; UNCOVERED_BRANCHES="-"
fi
-
# ===== Summary schreiben =====
{
echo "# SonarCloud – Voller Projekt-Report"
@@ -170,7 +159,6 @@ jobs:
echo "**Commit:** ${GITHUB_SHA:0:7} "
echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${KEY}](${SONAR_HOST}/project/overview?id=${KEY})"
echo ""
-
# --- Übersicht ---
echo "## Projekt-Übersicht"
echo ""
@@ -184,7 +172,6 @@ jobs:
echo "| Funktionen | $(get functions) |"
echo "| Statements | $(get statements) |"
echo ""
-
# --- Coverage (aus pytest-cov coverage.xml, nicht aus Sonar-API) ---
echo "## Test-Coverage (Backend / Python)"
echo ""
@@ -202,7 +189,6 @@ jobs:
echo "| Gedeckte Branches | ${BRANCHES_COVERED} |"
echo "| Ungedeckte Branches | ${UNCOVERED_BRANCHES} |"
echo ""
-
# --- Duplikate ---
echo "## Duplikate"
echo ""
@@ -213,7 +199,6 @@ jobs:
echo "| Duplikat-Blöcke | $(get duplicated_blocks) |"
echo "| Betroffene Dateien | $(get duplicated_files) |"
echo ""
-
# --- Komplexität ---
echo "## Komplexität"
echo ""
@@ -222,7 +207,6 @@ jobs:
echo "| Zyklomatische Komplexität | $(get complexity) |"
echo "| Cognitive Complexity | $(get cognitive_complexity) |"
echo ""
-
# --- Issues & Ratings ---
echo "## Issues & Bewertungen"
echo ""
@@ -233,7 +217,6 @@ jobs:
echo "| Maintainability (Code Smells) | $(rating sqale_rating) | $(get code_smells) | ${DEBT_FMT} |"
echo "| Security Hotspots | $(rating security_review_rating) | $(get security_hotspots) | $(get security_hotspots_reviewed)% reviewed |"
echo ""
-
# --- Technische Schuld ---
echo "## Technische Schuld"
echo ""
@@ -243,7 +226,6 @@ jobs:
echo "| Debt Ratio | $(get sqale_debt_ratio)% |"
echo ""
} >> "$OUT"
-
# =====================================================
# Top-10 Files: höchste Komplexität, schlechteste Coverage
# =====================================================
@@ -253,7 +235,6 @@ jobs:
echo "| Datei | Komplexität | Cognitive | LoC |"
echo "|---|---:|---:|---:|"
} >> "$OUT"
-
curl -s -u "${SONAR_TOKEN}:" \
"${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=complexity,cognitive_complexity,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=complexity&asc=false" \
| jq -r '.components[]?
@@ -263,7 +244,6 @@ jobs:
| ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
| "| " + $p + " | " + $c + " | " + $cog + " | " + $n + " |"' \
>> "$OUT"
-
{
echo ""
echo "## Coverage-Übersicht aller Backend-Dateien"
@@ -273,7 +253,6 @@ jobs:
echo "| Datei | Coverage | Gedeckte / Deckbare | Ungedeckte Zeilen |"
echo "|---|---:|---:|---:|"
} >> "$OUT"
-
if [ -f "${COV_FILE}" ]; then
python3 - "${COV_FILE}" >> "$OUT" <<'PYEOF'
import sys, xml.etree.ElementTree as ET
@@ -301,7 +280,6 @@ jobs:
else
echo "_coverage.xml nicht gefunden._" >> "$OUT"
fi
-
{
echo ""
echo "## Top-10 Dateien mit den meisten Duplikat-Blöcken"
@@ -309,7 +287,6 @@ jobs:
echo "| Datei | Blöcke | Dupl. Zeilen | LoC |"
echo "|---|---:|---:|---:|"
} >> "$OUT"
-
curl -s -u "${SONAR_TOKEN}:" \
"${SONAR_HOST}/api/measures/component_tree?component=${KEY}&metricKeys=duplicated_blocks,duplicated_lines,ncloc&qualifiers=FIL&ps=10&s=metric&metricSort=duplicated_blocks&asc=false" \
| jq -r '.components[]?
@@ -320,9 +297,18 @@ jobs:
| select(($b | tonumber) > 0)
| "| " + $p + " | " + $b + " | " + $l + " | " + $n + " |"' \
>> "$OUT"
-
{
echo ""
echo "---"
echo "_Generiert von SonarCloud-Analyse aus Push auf \`${GITHUB_REF_NAME}\`._"
} >> "$OUT"
+
+ - name: Fail job on Quality Gate failure
+ if: always()
+ env:
+ QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
+ run: |
+ if [ "${QG_STATUS}" != "PASSED" ]; then
+ echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
+ exit 1
+ fi
\ No newline at end of file
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 5b24b8a..a82a5f3 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -259,6 +259,7 @@ jobs:
echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
echo "| Technische Schuld | ${DEBT_FMT} | – |"
} >> "$SUMMARY_FILE"
+
- name: Fail job on Quality Gate failure
if: always()
env:
From 77cd468faddc9484c7085a2da829cc88c66043e5 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 26 May 2026 17:40:56 +0200
Subject: [PATCH 84/85] Fix: Code Preview from Copilot fixes
---
.github/workflows/sonarqube-main.yml | 6 +++---
.github/workflows/sonarqube.yml | 6 +++---
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/sonarqube-main.yml b/.github/workflows/sonarqube-main.yml
index d24aee3..ca301a3 100644
--- a/.github/workflows/sonarqube-main.yml
+++ b/.github/workflows/sonarqube-main.yml
@@ -25,7 +25,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: "3.10"
+ python-version: "3.11"
- name: Install backend deps
working-directory: project/backend
@@ -55,13 +55,13 @@ jobs:
-Dsonar.organization=galacticcodegambit
-Dproject.settings=sonar-project.properties
-Dsonar.python.coverage.reportPaths=backend/coverage.xml
- -Dsonar.python.version=3.10
+ -Dsonar.python.version=3.11
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Wait for analysis
id: qg
- uses: SonarSource/sonarqube-quality-gate-action@master
+ uses: SonarSource/sonarqube-quality-gate-action@v1.2.0
timeout-minutes: 5
continue-on-error: true
with:
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index a82a5f3..e760e9c 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -25,7 +25,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: "3.10"
+ python-version: "3.11"
- name: Install backend dependencies
working-directory: project/backend
@@ -55,14 +55,14 @@ jobs:
-Dsonar.organization=galacticcodegambit
-Dproject.settings=sonar-project.properties
-Dsonar.python.coverage.reportPaths=backend/coverage.xml
- -Dsonar.python.version=3.10
+ -Dsonar.python.version=3.11
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: SonarCloud Quality Gate check
id: sonar-qg
- uses: SonarSource/sonarqube-quality-gate-action@master
+ uses: SonarSource/sonarqube-quality-gate-action@v1.2.0
timeout-minutes: 5
continue-on-error: true
with:
From 5a7f7173946becf550a1b2a049d564a951263947 Mon Sep 17 00:00:00 2001
From: Eden Bernhard
Date: Tue, 26 May 2026 17:46:53 +0200
Subject: [PATCH 85/85] Fix: Code Preview from Copilot fixes
---
.github/workflows/sonarqube-main.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/sonarqube-main.yml b/.github/workflows/sonarqube-main.yml
index ca301a3..8bb652b 100644
--- a/.github/workflows/sonarqube-main.yml
+++ b/.github/workflows/sonarqube-main.yml
@@ -306,7 +306,7 @@ jobs:
- name: Fail job on Quality Gate failure
if: always()
env:
- QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
+ QG_STATUS: ${{ steps.qg.outputs.quality-gate-status }}
run: |
if [ "${QG_STATUS}" != "PASSED" ]; then
echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."