From ea628b9a0a915e9b6ac7e22e19a4c7a66e5f6cab Mon Sep 17 00:00:00 2001 From: Andrej Spielmann Date: Tue, 21 Apr 2026 10:53:28 +0200 Subject: [PATCH] Initial commit --- .DS_Store | Bin 0 -> 14340 bytes API/API Keys.md | 3 + Notes/.DS_Store | Bin 0 -> 6148 bytes Notes/.obsidian/app.json | 1 + Notes/.obsidian/appearance.json | 1 + Notes/.obsidian/community-plugins.json | 1 + Notes/.obsidian/core-plugins.json | 33 + Notes/.obsidian/graph.json | 22 + Notes/.obsidian/workspace-mobile.json | 176 ++++ Notes/.obsidian/workspace.json | 211 ++++ Notes/.trash/.DS_Store | Bin 0 -> 6148 bytes .../.trash/2026-03-03 Erwachsene Training.md | 13 + Notes/.trash/2026-03-03 Kindertraining.md | 18 + Notes/.trash/Kinder/2026-03-03.md | 18 + Notes/.trash/Kinder/2026-03-10.md | 16 + .../2026-03-03 Erwachsene Training.md | 13 + .../Trainings 2/Erwachsene/2026-03-03.md | 13 + .../2026-03-03 Erwachsene Training.md | 13 + .../Trainings/Training am Donnerstag.md | 0 Notes/AgentMail_Konfiguration.md | 47 + .../API_Dokumentation_Context7.md | 181 ++++ .../Dokumentenanalyse_Umbenennung.md | 174 ++++ .../PaperlessNGX/PaperlessNGX_Anweisungen.md | 89 ++ Notes/PaperlessNGX/PaperlessNGX_Log.md | 131 +++ Notes/SQLHosting/Anmeldedaten.md | 25 + Notes/SQLHosting/Design.md.md | 623 ++++++++++++ Notes/SQLHosting/Projektbeschreibung.md | 905 ++++++++++++++++++ Notes/Trainings/.DS_Store | Bin 0 -> 6148 bytes Notes/Trainings/Erwachsene/2026-03-03.md | 13 + Notes/Trainings/Kinder/2026-03-03.md | 18 + 30 files changed, 2758 insertions(+) create mode 100644 .DS_Store create mode 100644 API/API Keys.md create mode 100644 Notes/.DS_Store create mode 100644 Notes/.obsidian/app.json create mode 100644 Notes/.obsidian/appearance.json create mode 100644 Notes/.obsidian/community-plugins.json create mode 100644 Notes/.obsidian/core-plugins.json create mode 100644 Notes/.obsidian/graph.json create mode 100644 Notes/.obsidian/workspace-mobile.json create mode 100644 Notes/.obsidian/workspace.json create mode 100644 Notes/.trash/.DS_Store create mode 100644 Notes/.trash/2026-03-03 Erwachsene Training.md create mode 100644 Notes/.trash/2026-03-03 Kindertraining.md create mode 100644 Notes/.trash/Kinder/2026-03-03.md create mode 100644 Notes/.trash/Kinder/2026-03-10.md create mode 100644 Notes/.trash/Trainings 2/Erwachsene/2026-03-03 Erwachsene Training.md create mode 100644 Notes/.trash/Trainings 2/Erwachsene/2026-03-03.md create mode 100644 Notes/.trash/Trainings/Erwachsene/2026-03-03 Erwachsene Training.md create mode 100644 Notes/.trash/Trainings/Training am Donnerstag.md create mode 100644 Notes/AgentMail_Konfiguration.md create mode 100644 Notes/PaperlessNGX/API_Dokumentation_Context7.md create mode 100644 Notes/PaperlessNGX/Dokumentenanalyse_Umbenennung.md create mode 100644 Notes/PaperlessNGX/PaperlessNGX_Anweisungen.md create mode 100644 Notes/PaperlessNGX/PaperlessNGX_Log.md create mode 100644 Notes/SQLHosting/Anmeldedaten.md create mode 100644 Notes/SQLHosting/Design.md.md create mode 100644 Notes/SQLHosting/Projektbeschreibung.md create mode 100644 Notes/Trainings/.DS_Store create mode 100644 Notes/Trainings/Erwachsene/2026-03-03.md create mode 100644 Notes/Trainings/Kinder/2026-03-03.md diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..076df713b0063eefa1a400e8d49e581939e7d810 GIT binary patch literal 14340 zcmeHNd2Ae48GqmSnu$Gld^SgSvd#8LQYY~h*GcSTy>^<~PU0xLw%5*Kc6Y}1klmSO zXV;E#18$)O^`cz;0~OJNiuy+hM^&}76oNz{v|Oqp)Y1wN{869+0iqnLMETx(v+LdU znmPhS3YwALyz_fA?>FB!^M1cwLI?yhS{os)gb)!E&6OHF-A#y>=6MJ0r)kAehAn`p z0TY%LLIxAoz(6!^MOTb1)gWLawJ*n1M*7IH0(-pV(l?%d>HB@<6_r(Dl~^sF3eH;d z!E_{@u>+A(6S{3K0xkkB0xkkB0xkmYiU9rg;vrV^+&#MpxCpoi2S>bPQJu*Zcc!k z6L78!4nqNUb+q%c;RO6Vch4>YE&?kNpjDJbtflyYO(Wg#Oejl=|cr@aaBhfKK zpPy11`zo=lVGiWzj4UuVsOd)>YTY?@{H6$rC3hPlb(`Wd@B8=u=9qjd%=V^;k-uLmb9q zRy>n1?J1=$7Kz6)@kmMg4cm5hboch%I&}2- zC8e&uL2i<-8J{z3BO2F7=M61AI}x#Cx~UoF%p^V^Gi^*8dP=H;7AaDfcv4I4LI z>rvI>(b-wTnW*vpjFDbYe4@y&u|>UJQl1h|YPilLmPTjPSdY3z-71PFXnTK3$1`u1 zZ&r6mN<<9Vh!vlrRc@CQTMC^)C*r2Nlk69z zFvdax7i06be)XWJTog~!3m=Zy>5-&vW^buq9Tde$dp45NN9NImvld50kv(HUV!hxPv8}J6<&ka;Z67}yaj&~Kv*YK3pIi&+$6LK?LvppBlHRf z1iugzLc*wUT9^@%LWZwG$Oi1`+^SeXKnXMdC4{l(yo7}3SBX8ZJUBFbc<9L06nn4M z(r9aJYQARubvNGJx_i(5K+Y$*3unDHKN?s76od^60Oljb&dbA6_KsqN=x0}pPNj-Q zvUrJ(-r|$JN(IIcuims3tCbi(yxOwE>s15{TV8Ez_sU8c2BpMoyLMm|oXXyPvaFP2 zc=EmjDprY8=^wypXt0U@d$<1ud4v3cyaj-6-vH~u11+!{_CX)Ic@Rd?)hA#QU0sJ6 zm<0n;kcK&!N5{VdJ`8uk-SAO(5IzZ?hR?u5==P7o7vPKV1e{0L{~9{~bMQQT3%(5( z;YIi%{0M%GLGV)yf?s0@{He&Tj~2Og(FZ3_X{TG?w;xBPywLFzR{r#H@>YJMzac_;=+D)D* zr6C%Lota5dsR2o{l?uc&dX6pR(dzo5amRJ-^m0>ZA&IoUak+ubbtt9l=82hof(}BmayPF@d8zb z;8m7;)}Y9QCu&fVk&7O|E?begPW8zJv{F=_l9+hj&YsVu`xJLN8w zQ&~yTxbm|{-lt}Ht)vX|#{F`$n&ZAmHYqUALL(1 zO{<{>YD+3IA&iu9rpP3~WQxo=_#iAXMdss-no>oElG4YRBJ(sn1J5!=hLh1(;0^dK z{GKT??+An`GS>>8RTLRrFok*HcHu6*wp>J(Qe?6b`R;0r=eEHblw+={xVu7XjJ(;K z4cFo@<|>Uy16ZkIii$_R{ss{vN2zeC+qPqMSgGVy&(2*UMyVpOs_g0LlrR>RGN-z; z8>_%8a!;>>QK^(W)y;iaC9LA{9K02$xu}RGUjF~3%a>MTdQo^hf@%zP^)ORoG^WQ; zHO6Lo3{_+9WO@u$V?M$37^=oRf_e;9W4_Gv7^=p66ZIIX#{7WkF;tED75onV0)KaY zUBm!z)=)}~$u7^$T?AYNTm)PMTm-Hr1S}I#vp6-UdnBI$tj=6w|9$023cw!r7%-P@ZutI;6>}G?U6ZZLO5%VRUV+oHd Z{zt(22ABW4{C~~=hFY8McJ=Z9e*^bSazy|D literal 0 HcmV?d00001 diff --git a/API/API Keys.md b/API/API Keys.md new file mode 100644 index 0000000..696edaa --- /dev/null +++ b/API/API Keys.md @@ -0,0 +1,3 @@ +Ollama Hermes_Websearch: 6875e5e8701c414b845a82fdbbf3f628.P5LAYphcXIHqd5MLICDu1Q1S + +Gitea Obsidian_MAC: 656323c4f143a33241a20c993c5832e9ba81608d \ No newline at end of file diff --git a/Notes/.DS_Store b/Notes/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..fc82959b4db6b33ce15b45e4939b5d53987a3cc8 GIT binary patch literal 6148 zcmeHLy>8nu7(BK{kiWr8fT9_y2WScy$doRsp`aOpAdNd@Ol%jn5!n)~23_?C9XnSS#sou5aE@6#s<3^9cfdRFuQ?!dcMY$QppT4m_hJ5adV^$|8o4jO)U&2Z z6!qhz&sHuF`Q6@+?{Auz!#`xEx}RBV5-RceVVXaZJxr ze4uwRbH>jR-s03%+}$R-kz3Nd_S$G(z;{$H<-59%Mff^;c!xtA@U6_~_oV9b&0Q1E z)r()(MsMwy#rJ^YE#$g7f0E95^IpG|qEBl?<~sZQq@GpM_;vgDys3;=Qn-NW>I+5R zvf!RAvASWj5mCVd1Twpgu$*K(GSK7qy1Vs4Nv3}w0! zO;=@)7|L{KePDT^#oVCj4rLD?%05}y6N=JL=lFr9Lxl!?_6~RlTnC<-+qUfg`_1Qn zx0Ao~4tNLtD+g37>P20CCA+s)zMSl}0m~VSgz$2Mx(k!tj`f9X#qz&`4z>kyK@2VC S23f=KKLoT5K6wZJsss0e(cUcp literal 0 HcmV?d00001 diff --git a/Notes/.obsidian/app.json b/Notes/.obsidian/app.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/Notes/.obsidian/app.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Notes/.obsidian/appearance.json b/Notes/.obsidian/appearance.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/Notes/.obsidian/appearance.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Notes/.obsidian/community-plugins.json b/Notes/.obsidian/community-plugins.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/Notes/.obsidian/community-plugins.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/Notes/.obsidian/core-plugins.json b/Notes/.obsidian/core-plugins.json new file mode 100644 index 0000000..639b90d --- /dev/null +++ b/Notes/.obsidian/core-plugins.json @@ -0,0 +1,33 @@ +{ + "file-explorer": true, + "global-search": true, + "switcher": true, + "graph": true, + "backlink": true, + "canvas": true, + "outgoing-link": true, + "tag-pane": true, + "footnotes": false, + "properties": true, + "page-preview": true, + "daily-notes": true, + "templates": true, + "note-composer": true, + "command-palette": true, + "slash-command": false, + "editor-status": true, + "bookmarks": true, + "markdown-importer": false, + "zk-prefixer": false, + "random-note": false, + "outline": true, + "word-count": true, + "slides": false, + "audio-recorder": false, + "workspaces": false, + "file-recovery": true, + "publish": false, + "sync": true, + "bases": true, + "webviewer": false +} \ No newline at end of file diff --git a/Notes/.obsidian/graph.json b/Notes/.obsidian/graph.json new file mode 100644 index 0000000..7db60ea --- /dev/null +++ b/Notes/.obsidian/graph.json @@ -0,0 +1,22 @@ +{ + "collapse-filter": true, + "search": "", + "showTags": false, + "showAttachments": false, + "hideUnresolved": false, + "showOrphans": true, + "collapse-color-groups": true, + "colorGroups": [], + "collapse-display": true, + "showArrow": false, + "textFadeMultiplier": 0, + "nodeSizeMultiplier": 1, + "lineSizeMultiplier": 1, + "collapse-forces": true, + "centerStrength": 0.518713248970312, + "repelStrength": 10, + "linkStrength": 1, + "linkDistance": 250, + "scale": 0.9874372290055001, + "close": true +} \ No newline at end of file diff --git a/Notes/.obsidian/workspace-mobile.json b/Notes/.obsidian/workspace-mobile.json new file mode 100644 index 0000000..f122ac1 --- /dev/null +++ b/Notes/.obsidian/workspace-mobile.json @@ -0,0 +1,176 @@ +{ + "main": { + "id": "62cf0038ff114e49", + "type": "split", + "children": [ + { + "id": "79e10c965f93ac09", + "type": "tabs", + "children": [ + { + "id": "437e9deae12a88ca", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "PaperlessNGX/PaperlessNGX_Log.md", + "mode": "source", + "source": false + }, + "icon": "lucide-file", + "title": "PaperlessNGX_Log" + } + } + ] + } + ], + "direction": "vertical" + }, + "left": { + "id": "ca71fb4d750506af", + "type": "mobile-drawer", + "children": [ + { + "id": "edb7c0914f082eda", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical", + "autoReveal": false + }, + "icon": "lucide-folder-closed", + "title": "Dateiexplorer" + } + }, + { + "id": "1360d4663c2db1d0", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "", + "matchingCase": false, + "explainSearch": false, + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical" + }, + "icon": "lucide-search", + "title": "Suchen" + } + }, + { + "id": "f5b2a77629417ecd", + "type": "leaf", + "state": { + "type": "tag", + "state": { + "sortOrder": "frequency", + "useHierarchy": true, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-tags", + "title": "Tags" + } + }, + { + "id": "20f16c8dc8f03e5e", + "type": "leaf", + "state": { + "type": "all-properties", + "state": { + "sortOrder": "frequency", + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-archive", + "title": "Alle Eigenschaften" + } + }, + { + "id": "0f0998d8770b6d35", + "type": "leaf", + "state": { + "type": "bookmarks", + "state": {}, + "icon": "lucide-bookmark", + "title": "Lesezeichen" + } + } + ], + "currentTab": 0 + }, + "right": { + "id": "285d83fd90346800", + "type": "mobile-drawer", + "children": [ + { + "id": "d96384a36ee9e67d", + "type": "leaf", + "state": { + "type": "backlink", + "state": { + "file": "SQLHosting/Anmeldedaten.md", + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical", + "showSearch": false, + "searchQuery": "", + "backlinkCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-coming-in", + "title": "Rückverweise" + } + }, + { + "id": "3dfa9f6ad917f680", + "type": "leaf", + "state": { + "type": "outgoing-link", + "state": { + "file": "Entwicklung/SQLHostingDesk/Projektbeschreibung.md", + "linksCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-going-out", + "title": "Ausgehende Links" + } + }, + { + "id": "e0575d30f1ece371", + "type": "leaf", + "state": { + "type": "outline", + "state": { + "file": "Entwicklung/SQLHostingDesk/Projektbeschreibung.md", + "followCursor": false, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-list", + "title": "Gliederung" + } + } + ], + "currentTab": 0 + }, + "left-ribbon": { + "hiddenItems": { + "switcher:Schnellauswahl öffnen": false, + "graph:Graph-Ansicht öffnen": false, + "canvas:Neuen Canvas erstellen": false, + "daily-notes:Heutige Notiz öffnen": false, + "templates:Vorlage einfügen": false, + "command-palette:Befehlspalette öffnen": false, + "bases:Neue Base erstellen": false + } + }, + "active": "437e9deae12a88ca", + "lastOpenFiles": [ + "SQLHosting/Anmeldedaten.md", + "Entwicklung/SQLHostingDesk/Projektbeschreibung.md" + ] +} \ No newline at end of file diff --git a/Notes/.obsidian/workspace.json b/Notes/.obsidian/workspace.json new file mode 100644 index 0000000..a19c492 --- /dev/null +++ b/Notes/.obsidian/workspace.json @@ -0,0 +1,211 @@ +{ + "main": { + "id": "8c62124cec9c5041", + "type": "split", + "children": [ + { + "id": "b085adecc4e71c8c", + "type": "tabs", + "children": [ + { + "id": "2b7532e8a6084d2c", + "type": "leaf", + "state": { + "type": "bases", + "state": { + "file": "Unbenannt.base", + "viewName": "Tabelle" + }, + "icon": "lucide-table", + "title": "Unbenannt" + } + } + ] + } + ], + "direction": "vertical" + }, + "left": { + "id": "66d8d1f5b340307d", + "type": "split", + "children": [ + { + "id": "564855d7574cb5eb", + "type": "tabs", + "children": [ + { + "id": "de7245107632f08f", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical", + "autoReveal": false + }, + "icon": "lucide-folder-closed", + "title": "Dateiexplorer" + } + }, + { + "id": "264963109e69afe5", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "", + "matchingCase": false, + "explainSearch": false, + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical" + }, + "icon": "lucide-search", + "title": "Suchen" + } + }, + { + "id": "3617eb6a65577779", + "type": "leaf", + "state": { + "type": "bookmarks", + "state": {}, + "icon": "lucide-bookmark", + "title": "Lesezeichen" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300 + }, + "right": { + "id": "6058dd084c583192", + "type": "split", + "children": [ + { + "id": "9548ea3ddd25834c", + "type": "tabs", + "children": [ + { + "id": "1b74be11690924a4", + "type": "leaf", + "state": { + "type": "backlink", + "state": { + "file": "Willkommen.md", + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical", + "showSearch": false, + "searchQuery": "", + "backlinkCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-coming-in", + "title": "Rückverweise auf Willkommen" + } + }, + { + "id": "54f544090f057bc8", + "type": "leaf", + "state": { + "type": "outgoing-link", + "state": { + "file": "Willkommen.md", + "linksCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-going-out", + "title": "Ausgehende Links von Willkommen öffnen" + } + }, + { + "id": "4a4052d0c9374ad1", + "type": "leaf", + "state": { + "type": "tag", + "state": { + "sortOrder": "frequency", + "useHierarchy": true, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-tags", + "title": "Tags" + } + }, + { + "id": "c63dcd43d454d9f5", + "type": "leaf", + "state": { + "type": "all-properties", + "state": { + "sortOrder": "frequency", + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-archive", + "title": "Alle Eigenschaften" + } + }, + { + "id": "26a3517baa0fb4db", + "type": "leaf", + "state": { + "type": "outline", + "state": { + "file": "Willkommen.md", + "followCursor": false, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-list", + "title": "Gliederung von Willkommen" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300, + "collapsed": true + }, + "left-ribbon": { + "hiddenItems": { + "switcher:Schnellauswahl öffnen": false, + "graph:Graph-Ansicht öffnen": false, + "canvas:Neuen Canvas erstellen": false, + "daily-notes:Heutige Notiz öffnen": false, + "templates:Vorlage einfügen": false, + "command-palette:Befehlspalette öffnen": false, + "bases:Neue Base erstellen": false + } + }, + "active": "2b7532e8a6084d2c", + "lastOpenFiles": [ + "Entwicklung/SQLHostingDesk/Projektbeschreibung.md", + "Unbenannt.base", + "Entwicklung/WrestleDesk/Frontend.md", + "Entwicklung/SQLHostingDesk", + "Unbenannt.md", + "Trainings/Kinder/2026-03-10.md", + "Trainings/Kinder/2026-03-03.md", + "Trainings/Erwachsene/2026-03-03.md", + "Unbenannt.canvas", + "Entwicklung/WrestleDesk", + "Entwicklung", + "00-Notizen-Präferenzen.md", + "Trainings/Kinder", + "Trainings/Erwachsene", + "Trainings", + "Trainings/Erwachsene/2026-03-03 Erwachsene Training.md", + "Trainings/Kinder/2026-03-03 Kindertraining.md", + "2026-03-02.md", + "Trainings/Training am Donnerstag.md", + "Trainings/2026-03-03 Erwachsene Training.md", + "Willkommen.md", + "Welcome.md", + "flag_fetch.md" + ] +} \ No newline at end of file diff --git a/Notes/.trash/.DS_Store b/Notes/.trash/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ca7195319cd15560e292244303bf9ef0c2f7ca7e GIT binary patch literal 6148 zcmeHKF;2rk5S%j+N)(Ee6cpFc6JB5)rEseD?VOxXDX721Wq-bir)Erp4sEc*C+7IU+h8;~wk9d_29Yma9g# z1^%J}^6U%Q!i zuNS^%rhqA63Un#JHCwDd3}~$>U<#N58wKS1;OT;S#5$mUI@stIfY@VLjeY%BB6)fxAi4wnbC))X)WQUwme z;Yim1%kKSuT4YD2fGO~=6mY#_T1 **Wichtig:** Die Plattformdatenbank ist bewusst mssql – keine SQL Server Eigenabhängigkeit. Alle Verbindungen zu Customer-SQL-Servern laufen ausschließlich über pyodbc. + +*** + +## 📊 Projekt-Status (Aktualisiert: 2026-04-20) + +### ✅ Implementiert + +| Komponente | Status | Details | +|------------|--------|---------| +| Backend Core | ✅ Fertig | Django 5.2, 17 Modelle, alle APIs | +| Frontend Foundation | ✅ Fertig | Next.js 16, 83+ TSX Dateien | +| Docker Setup | ✅ Fertig | 6 Services (mssql, redis, django, celery, celery-beat, nextjs) | +| Authentifizierung | ✅ Fertig | JWT mit simplejwt, Custom User Model | +| Admin Interface | ✅ Fertig | Server Pools, Server, Users, Query Logs | +| Customer Interface | ✅ Fertig | Dashboard, Applications, Booking Wizard | +| API Integration | ✅ Fertig | TanStack Query, BFF Pattern | +| Test Suite | ✅ Fertig | 99+ Backend Tests, Vitest, Playwright E2E | +| Dokumentation | ✅ Fertig | README, API-Docs, Deployment Guide | + +### 🔄 In Entwicklung + +| Komponente | Status | Blocker | +|------------|--------|---------| +| OpenCode Frontend | 🔄 Läuft | Background Task, ~83 Dateien erstellt | + +### 📝 Noch Offen + +- [ ] Produktions-Deployment +- [ ] SSL/TLS Konfiguration +- [ ] Backup-Automatisierung +- [ ] Monitoring & Alerting + +### 📁 Repository Struktur + +``` +sqlserver-platform/ +├── backend/ +│ ├── apps/ +│ │ ├── user_auth/ +│ │ ├── serverpool/ +│ │ ├── applications/ +│ │ ├── provisioning/ +│ │ └── auditlog/ +│ ├── config/ +│ ├── tests/ +│ │ ├── unit/ +│ │ └── integration/ +│ └── requirements.txt +├── frontend/ +│ ├── src/ +│ │ ├── app/ +│ │ │ ├── (admin)/ +│ │ │ ├── (auth)/ +│ │ │ ├── (customer)/ +│ │ │ └── api/ +│ │ ├── components/ +│ │ ├── services/ +│ │ └── lib/ +│ ├── tests/e2e/ +│ └── playwright.config.ts +├── docker-compose.dev.yml +├── docker-compose.yml +├── README.md +├── API_DOCUMENTATION.md +├── DEPLOYMENT.md +└── CONTRIBUTING.md +``` + +### 🐳 Docker Services + +| Service | Port | Beschreibung | +|---------|------|--------------| +| mssql | 1433 | Microsoft SQL Server 2019+ | +| redis | 6379 | Cache & Celery Broker | +| django | 8000 | Django REST API | +| celery-worker | - | Async Task Processing | +| celery-beat | - | Scheduled Tasks | +| nextjs-dev | 3000 | Next.js Frontend | + +### 🔑 Test Credentials + +- **Admin:** admin@sqlhosting.com / Admin123! +- **Backend Health:** http://localhost:8000/api/v1/health/ + +*** + +## Systemarchitektur + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ Next.js Frontend │ +│ Admin Panel │ Kunden Dashboard │ +└───────────────────────────────────┬──────────────────────────────┘ + │ REST API (JWT) +┌───────────────────────────────────▼──────────────────────────────┐ +│ Django REST API │ +│ Auth │ ServerPool │ Application │ Booking │ QueryLog │ AuditLog │ +└──────────────┬────────────────────────────────┬──────────────────┘ + │ │ + ┌──────────▼──────────┐ ┌────────────▼────────────┐ + │ MSSQL │ │ Celery + Redis │ + │ (Plattform-DB) │ │ Discovery, Health, │ + └─────────────────────┘ │ Provisioning, Quota │ + └────────────┬────────────┘ + │ pyodbc + ┌───────────────────▼──────────────────┐ + │ Customer SQL Server │ + │ Standalone │ HA (Always On) │ + └───────────────────────────────────────┘ +``` + +*** + +## Kernkonzept: Applikation als Container + +Das zentrale Objekt aus Kundensicht ist die **Applikation** – nicht die einzelne Datenbank. Eine Applikation ist ein logischer Container, der dem Kunden gehört, einen Namen trägt und einem HA-Typ (Standalone oder High Availability) zugeordnet ist. + +- Bei **Standalone** ist die Applikation eine logische Gruppe von Datenbanken auf demselben Server +- Bei **High Availability** entspricht die Applikation genau **einer Availability Group** – die AG wird nach der Applikation benannt (`AG_`) +- Der Kunde erstellt zuerst eine Applikation (inkl. erster Datenbank), kann danach weitere Datenbanken zur selben Applikation hinzufügen +- Neue Datenbanken einer HA-Applikation werden zur **bestehenden AG hinzugefügt** (`ALTER AVAILABILITY GROUP ... ADD DATABASE`) – es wird keine neue AG erstellt + +*** + +## Modul 1 – Benutzerverwaltung & Authentifizierung + +### Rollen & Aktivierung +Das System kennt zwei Rollen: `ADMIN` und `CUSTOMER`. Ein neuer Kunde registriert sich und landet im Status `is_approved = False` ohne Zugangsberechtigung, bis der Admin ihn manuell freischaltet. Der Admin sieht im Panel alle ausstehenden Aktivierungen mit Registrierungsdatum und kann per Klick freischalten oder ablehnen. + +**Django-Modelle:** +```python +class User(AbstractUser): + role = CharField(choices=["ADMIN", "CUSTOMER"]) + is_approved = BooleanField(default=False) + approved_by = FK(User, null=True) + approved_at = DateTimeField(null=True) + +class CustomerProfile(Model): + user = OneToOneField(User) + company_name = CharField() + contact_phone = CharField() + created_at = DateTimeField() +``` + +**API-Endpunkte:** +``` +POST /api/auth/register/ +POST /api/auth/login/ +POST /api/auth/token/refresh/ +GET /api/admin/users/pending/ +PATCH /api/admin/users/{id}/approve/ +PATCH /api/admin/users/{id}/reject/ +``` + +*** + +## Modul 2 – Server Pool Management + +### Pool-Typen +Der Admin verwaltet Server in Pools. Es gibt zwei Typen: + +**Typ 1 – Standalone:** Ein einzelner SQL Server. Datenbanken laufen ohne Redundanz. + +**Typ 2 – High Availability:** Zwei SQL Server werden als Pärchen hinzugefügt (Primary/Secondary in einer Always On Availability Group). Primär und Sekundär können nach Failover wechseln. + +**Django-Modelle:** +```python +class ServerPool(Model): + name = CharField() + pool_type = CharField(choices=["STANDALONE", "HA"]) + description = TextField() + is_active = BooleanField() + created_at = DateTimeField() + +class Server(Model): + pool = FK(ServerPool) + hostname = CharField() + ip_address = FK(IPAddress) + sql_instance = CharField() # z.B. HOSTNAME\INSTANCE + # Discovery – Ressourcen + total_ram_gb = DecimalField() + total_cpu_cores = IntegerField() + total_disk_gb = DecimalField() + free_ram_gb = DecimalField() + free_disk_gb = DecimalField() + # Discovery – SQL Server Version + sql_version_full = CharField() # z.B. 16.0.4135.4 + sql_version_major = IntegerField() # z.B. 16 + sql_version_level = CharField() # RTM / CU14 / SP1 + sql_edition = CharField() # Enterprise / Standard + # Discovery – HA-relevante Felder + collation = CharField() + endpoint_name = CharField() + endpoint_port = IntegerField() + server_fqdn = CharField() + # Status + last_checked = DateTimeField() + is_active = BooleanField() + discovery_status = CharField() # PENDING / OK / ERROR + +class HAServerPair(Model): + pool = FK(ServerPool) + primary_server = FK(Server) + secondary_server = FK(Server) + listener = FK(AGListener) # muss Admin vorher zuweisen + is_ready = BooleanField() # True wenn Listener zugewiesen +``` + +**API-Endpunkte:** +``` +GET /api/admin/server-pools/ +POST /api/admin/server-pools/ +POST /api/admin/server-pools/{id}/add-server/ +POST /api/admin/server-pools/{id}/add-ha-pair/ +POST /api/admin/servers/{id}/discover/ +GET /api/admin/servers/{id}/status/ +``` + +*** + +## Modul 3 – Server Discovery + +Beim Hinzufügen eines Servers wird sofort ein Celery-Task ausgelöst. Alle dabei ausgeführten SQL-Queries werden automatisch im **Query Log** gespeichert. + +### Discovery-Queries + +```sql +-- 1. RAM & CPU +SELECT physical_memory_kb / 1024 / 1024 AS total_ram_gb, + cpu_count +FROM sys.dm_os_sys_info; + +-- 2. Disk Space +SELECT volume_mount_point, + total_bytes / 1073741824.0 AS total_gb, + available_bytes / 1073741824.0 AS free_gb +FROM sys.dm_os_volume_stats(DB_ID('master'), 1); + +-- 3. SQL Server Version +SELECT SERVERPROPERTY('ProductVersion') AS version_full, + SERVERPROPERTY('ProductMajorVersion') AS version_major, + SERVERPROPERTY('ProductLevel') AS version_level, + SERVERPROPERTY('Edition') AS edition; + +-- 4. Collation +SELECT SERVERPROPERTY('Collation') AS server_collation; + +-- 5. HA Endpoint (nur für HA-Server) +SELECT name, port +FROM sys.tcp_endpoints +WHERE type_desc = 'DATABASE_MIRRORING'; + +-- 6. Server FQDN +SELECT SERVERPROPERTY('MachineName') AS machine_name, + DEFAULT_DOMAIN() AS domain; + +-- 7. IP-Adresse (zur Zuordnung in IP-Pool) +SELECT local_net_address +FROM sys.dm_exec_connections +WHERE session_id = @@SPID; +``` + +### SQL Server Version → Anzeigename & AG-Template + +| `version_major` | Anzeigename | AG-Template | +|---|---|---| +| 13 | SQL Server 2016 | `BASIC` Availability Groups | +| 14 | SQL Server 2017 | Standard AG | +| 15 | SQL Server 2019 | Standard AG | +| 16 | SQL Server 2022 | Standard AG / `CONTAINED` AG möglich | + +Die Major-Version steuert welches Query-Template bei der AG-Erstellung verwendet wird. Templates werden pro `query_type` + `sql_version_major` in der Datenbank hinterlegt. + +*** + +## Modul 4 – Scheduled Health Checks + +Ein Celery-Beat-Task prüft regelmäßig alle aktiven Server auf freie Ressourcen. Intervall ist im Admin konfigurierbar. + +### Admin-Konfiguration: `ServerSelectionThreshold` + +```python +class ServerSelectionThreshold(Model): + server_pool = FK(ServerPool) + min_free_disk_gb = DecimalField() # z.B. 10 GB müssen frei sein + min_free_ram_gb = DecimalField() + min_free_cpu_percent = IntegerField() # z.B. 20 % CPU muss frei sein + health_check_interval = IntegerField() # Minuten +``` + +Beim Buchungsprozess wählt das System automatisch den passenden Server/Pair anhand dieser Schwellwerte. Ein Server, der die Thresholds nicht erfüllt, wird automatisch aus der Auswahl ausgeschlossen. + +*** + +## Modul 5 – IP-Adresspool + +Der Admin legt Netzwerke manuell an. IPs werden bei der Server-Discovery automatisch dem passenden Netz zugeordnet. Für AG-Listener wird die nächste freie IP aus dem passenden Netz automatisch vergeben. + +```python +class IPNetwork(Model): + cidr = CIDRField() # z.B. 10.10.1.0/24 + description = CharField() + vlan_id = IntegerField(null=True) + created_at = DateTimeField() + +class IPAddress(Model): + address = GenericIPAddressField() + network = FK(IPNetwork) + purpose = CharField(choices=["SERVER", "LISTENER", "FREE"]) + assigned_to_type = CharField(null=True) # GenericFK + assigned_to_id = IntegerField(null=True) + reserved_at = DateTimeField(null=True) +``` + +**API-Endpunkte:** +``` +GET /api/admin/ip-networks/ +POST /api/admin/ip-networks/ +POST /api/admin/ip-networks/{id}/add-ip-range/ +GET /api/admin/ip-addresses/ +PATCH /api/admin/ip-addresses/{id}/assign/ +``` + +*** + +## Modul 6 – AG-Listener Verwaltung + +Bevor ein HA-ServerPair für Kundenbuchungen verfügbar ist, **muss der Admin einen Listener zuweisen**. Erst dann wird `HAServerPair.is_ready = True`. Der Listener besteht aus Name, IP (aus dem IP-Pool) und Port. + +```python +class AGListener(Model): + name = CharField() # z.B. AG-LISTENER-01 + ip_address = FK(IPAddress) + port = IntegerField() # Standard: 1433 + subnet_mask = CharField() + assigned_to = OneToOneField(HAServerPair, null=True) + created_at = DateTimeField() +``` + +**API-Endpunkte:** +``` +GET /api/admin/ag-listeners/ +POST /api/admin/ag-listeners/ +PATCH /api/admin/ha-pairs/{id}/assign-listener/ +``` + +*** + +## Modul 7 – Service Account Isolation + +Pro **ServerPool** wird ein dedizierter Service Account konfiguriert. Ein kompromittierter Account hat damit nur Zugriff auf seinen eigenen Pool. Das Passwort wird mit **Fernet-Verschlüsselung** gespeichert (Key in `.env`). + +```python +class ElevatedAccount(Model): + server_pool = OneToOneField(ServerPool) + username = CharField() + password_encrypted = BinaryField() # Fernet encrypted + last_verified = DateTimeField() + permissions_ok = BooleanField() # dbcreator + securityadmin + last_permission_check = DateTimeField() +``` + +Ein Celery-Task prüft regelmäßig ob der Account noch `dbcreator` und `securityadmin` besitzt. Fehlt die Berechtigung, wird der Pool für neue Buchungen gesperrt und ein Audit-Log-Eintrag erstellt. + +*** + +## Modul 8 – Kundenbuchungsprozess + +### Konzept: Applikation → Datenbank + +``` +Applikation (Container) +├── Name: "MeinShop" ← vom Kunden vergeben +├── Typ: HA ← bestimmt AG-Name: AG_MeinShop +├── Server/Pair: automatisch ← anhand Thresholds gewählt +├── Collation: SQL_Latin1_... ← aus Discovery des gewählten Pools +│ +├── Datenbank: "MeinShop_Prod" ← erste DB bei Erstellung +├── Datenbank: "MeinShop_Dev" ← später hinzugefügt +└── Datenbank: "MeinShop_Log" ← später hinzugefügt +``` + +### Buchungs-Wizard – Neue Applikation (5 Schritte) + +``` +Schritt 1: Applikationsname + ┌─────────────────────────────────────────────┐ + │ Applikationsname: [ MeinShop ] │ + │ (Nur Buchstaben, Zahlen, Unterstriche; │ + │ wird Teil des AG-Namens bei HA) │ + └─────────────────────────────────────────────┘ + +Schritt 2: HA-Typ wählen + ┌───────────────────┐ ┌──────────────────────────────┐ + │ Standalone │ │ High Availability │ + │ SQL Server 2022 │ │ SQL Server 2022 Enterprise │ + │ Standard │ │ Always On – 2 Nodes │ + └───────────────────┘ └──────────────────────────────┘ + +Schritt 3: SQL Server Collation wählen + Dropdown – befüllt aus discovered Collations der + verfügbaren Server im gewählten Pool-Typ + +Schritt 4: Name der ersten Datenbank + Größe (GB) + ┌─────────────────────────────────────────────┐ + │ Datenbankname: [ MeinShop_Prod ] │ + │ Größe (GB): [ 10 ] │ + └─────────────────────────────────────────────┘ + +Schritt 5: Zusammenfassung + "Jetzt buchen" + Anzeige: Applikation, Typ, Collation, Version, + DB-Name, Größe, ggf. AG-Name +``` + +### Weitere Datenbank zu bestehender Applikation hinzufügen + +``` +Kunde → "Applikation MeinShop" → "Neue Datenbank hinzufügen" + +Schritt 1: Datenbankname + Größe +Schritt 2: Zusammenfassung + "Erstellen" + +→ Bei HA: ALTER AVAILABILITY GROUP [AG_MeinShop] ADD DATABASE [neuer_name] +→ Bei Standalone: CREATE DATABASE auf demselben Server wie die Applikation +``` + +### Django-Modelle + +```python +class CustomerApplication(Model): + customer = FK(CustomerProfile) + app_name = CharField() # z.B. "MeinShop" + ha_type = CharField(choices=["STANDALONE", "HA"]) + # Zugewiesener Server/Pair (bei Erstellung automatisch gewählt) + server = FK(Server, null=True) # Standalone + ha_pair = FK(HAServerPair, null=True) # HA + collation = CharField() + # HA-spezifisch + ag_name = CharField(null=True) # = "AG_" + app_name + ag_created = BooleanField(default=False) + # Status + status = CharField(choices=[ + "PROVISIONING", "ACTIVE", + "ERROR", "DELETED" + ]) + created_at = DateTimeField() + deleted_at = DateTimeField(null=True) + +class CustomerDatabase(Model): + application = FK(CustomerApplication) # ← Neu: gehört zur App + customer = FK(CustomerProfile) + db_name = CharField() + size_gb = IntegerField() + connection_string = CharField() + # Soft Delete + status = CharField(choices=[ + "ACTIVE", "DELETION_REQUESTED", + "DELETION_CONFIRMED", "DELETED" + ]) + created_at = DateTimeField() + deleted_at = DateTimeField(null=True) + deletion_confirmed_at = DateTimeField(null=True) + drop_executed_at = DateTimeField(null=True) + deletion_token = UUIDField(null=True) +``` + +### Automatische Serverauswahl (Backend-Logik) + +```python +def select_server(pool_type, required_size_gb, collation): + threshold = ServerSelectionThreshold.objects.get( + server_pool__pool_type=pool_type + ) + candidates = Server.objects.filter( + pool__pool_type=pool_type, + collation=collation, + free_disk_gb__gte=required_size_gb + threshold.min_free_disk_gb, + free_ram_gb__gte=threshold.min_free_ram_gb, + is_active=True + ).order_by('free_disk_gb') # kleinsten passenden nehmen + return candidates.first() +``` + +**API-Endpunkte:** +``` +GET /api/customer/applications/ +POST /api/customer/applications/ # neue App + erste DB +GET /api/customer/applications/{id}/ +POST /api/customer/applications/{id}/databases/ # weitere DB hinzufügen +GET /api/customer/applications/{id}/databases/ +DELETE /api/customer/applications/{id}/delete-request/ +DELETE /api/customer/applications/{id}/delete-confirm/ +``` + +*** + +## Modul 9 – Datenbankbereitstellung (Celery-Task) + +Nach Buchung läuft ein Celery-Task, der via pyodbc alle nötigen SQL-Schritte ausführt. Alle Queries werden im Query Log gespeichert. + +### Fall A: Neue Standalone-Applikation (erste DB) + +```sql +CREATE DATABASE [{{db_name}}] COLLATE {{collation}}; +ALTER DATABASE [{{db_name}}] + MODIFY FILE (NAME = '{{db_name}}', SIZE = {{size_gb}}GB); +``` + +### Fall B: Weitere DB zu bestehender Standalone-Applikation + +```sql +-- Auf demselben Server wie die Applikation +CREATE DATABASE [{{db_name}}] COLLATE {{collation}}; +ALTER DATABASE [{{db_name}}] + MODIFY FILE (NAME = '{{db_name}}', SIZE = {{size_gb}}GB); +``` + +### Fall C: Neue HA-Applikation (AG + erste DB) + +```sql +-- 1. DB auf Primary erstellen +CREATE DATABASE [{{db_name}}] COLLATE {{collation}}; + +-- 2. Recovery Model setzen (AG-Voraussetzung) +ALTER DATABASE [{{db_name}}] SET RECOVERY FULL; + +-- 3. Full Backup (AG-Voraussetzung) +BACKUP DATABASE [{{db_name}}] + TO DISK = '\\{{backup_share}}\{{db_name}}.bak' + WITH FORMAT, INIT; + +-- 4. Availability Group erstellen (SQL 2022 Template) +CREATE AVAILABILITY GROUP [AG_{{app_name}}] +WITH ( + DB_FAILOVER = ON, + REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT = 0 +) +FOR DATABASE [{{db_name}}] +REPLICA ON + '{{primary_fqdn}}' WITH ( + ENDPOINT_URL = 'TCP://{{ep1_host}}:{{ep1_port}}', + AVAILABILITY_MODE = SYNCHRONOUS_COMMIT, + FAILOVER_MODE = AUTOMATIC, + SEEDING_MODE = AUTOMATIC, + SECONDARY_ROLE (ALLOW_CONNECTIONS = ALL) + ), + '{{secondary_fqdn}}' WITH ( + ENDPOINT_URL = 'TCP://{{ep2_host}}:{{ep2_port}}', + AVAILABILITY_MODE = SYNCHRONOUS_COMMIT, + FAILOVER_MODE = AUTOMATIC, + SEEDING_MODE = AUTOMATIC, + SECONDARY_ROLE (ALLOW_CONNECTIONS = ALL) + ); + +-- 5. Listener hinzufügen +ALTER AVAILABILITY GROUP [AG_{{app_name}}] +ADD LISTENER '{{listener_name}}' ( + WITH IP (('{{listener_ip}}', '{{subnet_mask}}')), + PORT = {{listener_port}} +); + +-- 6. Secondary joinen (läuft auf Secondary-Server) +ALTER AVAILABILITY GROUP [AG_{{app_name}}] JOIN; +ALTER AVAILABILITY GROUP [AG_{{app_name}}] + GRANT CREATE ANY DATABASE; +``` + +### Fall D: Weitere DB zu bestehender HA-Applikation + +```sql +-- 1. DB auf Primary erstellen (AG bereits vorhanden) +CREATE DATABASE [{{db_name}}] COLLATE {{collation}}; +ALTER DATABASE [{{db_name}}] SET RECOVERY FULL; + +-- 2. Backup +BACKUP DATABASE [{{db_name}}] + TO DISK = '\\{{backup_share}}\{{db_name}}.bak' + WITH FORMAT, INIT; + +-- 3. Zur bestehenden AG hinzufügen +ALTER AVAILABILITY GROUP [AG_{{app_name}}] + ADD DATABASE [{{db_name}}]; +-- Kein neuer Listener nötig – AG_{{app_name}} existiert bereits +``` + +*** + +## Modul 10 – Soft Delete (Doppelte Bestätigung) + +Das Löschen kann auf zwei Ebenen erfolgen: **Einzelne Datenbank** oder **gesamte Applikation**. Beide Ebenen durchlaufen den gleichen 3-stufigen Prozess. + +``` +STUFE 1 – Löschanfrage + Kunde klickt "Löschen" (DB oder App) + → Modal: "Bitte Namen eingeben zur Bestätigung" + → POST /api/customer/applications/{id}/delete-request/ + oder POST /api/customer/databases/{id}/delete-request/ + → Status: DELETION_REQUESTED + → Karenzzeit: deleted_at = jetzt + 48h + → Audit Log: DELETE_REQUEST + → E-Mail (wenn aktiv): Bestätigungs-Link + +STUFE 2 – Finale Bestätigung + Dashboard-Banner: "Löschung ausstehend" + Kunde klickt "Endgültig löschen" + gibt Namen erneut ein + → POST .../delete-confirm/ + → Status: DELETION_CONFIRMED + → Audit Log: DELETE_CONFIRMED + +STUFE 3 – Celery-Task Ausführung + → Einzelne DB Standalone: + DROP DATABASE [{{db_name}}] + + → Einzelne DB HA: + ALTER AVAILABILITY GROUP [AG_{{app_name}}] + REMOVE DATABASE [{{db_name}}]; + DROP DATABASE [{{db_name}}]; + + → Gesamte Applikation HA: + DROP AVAILABILITY GROUP [AG_{{app_name}}]; + DROP DATABASE [{{db_name}}]; -- für jede DB der App + + → Status: DELETED, drop_executed_at = timestamp + → Audit Log: DELETE_EXECUTED + → E-Mail (wenn aktiv): Bestätigung der Löschung +``` + +*** + +## Modul 11 – Login-Verwaltung (Kundenseite) + +Der Kunde kann für seine Datenbanken SQL Server Logins erstellen und verwalten. Alle Aktionen laufen über den `ElevatedAccount` des zugehörigen Pools. Der Kunde sieht ausschließlich Datenbanken und Logins, die seiner `CustomerProfile`-ID gehören. + +```sql +-- Login erstellen +CREATE LOGIN [{{login_name}}] + WITH PASSWORD = '{{password}}', + DEFAULT_DATABASE = [{{db_name}}]; + +USE [{{db_name}}]; +CREATE USER [{{login_name}}] FOR LOGIN [{{login_name}}]; +ALTER ROLE db_datareader ADD MEMBER [{{login_name}}]; +ALTER ROLE db_datawriter ADD MEMBER [{{login_name}}]; + +-- Login löschen +USE [{{db_name}}]; +DROP USER [{{login_name}}]; +USE master; +DROP LOGIN [{{login_name}}]; +``` + +**Django-Modell `DatabaseLogin`:** +```python +class DatabaseLogin(Model): + database = FK(CustomerDatabase) + login_name = CharField() + created_at = DateTimeField() + created_by = FK(User) + is_active = BooleanField() +``` + +**API-Endpunkte:** +``` +GET /api/customer/databases/{id}/logins/ +POST /api/customer/databases/{id}/logins/ +DELETE /api/customer/databases/{id}/logins/{login_id}/ +``` + +*** + +## Modul 12 – Query Log & Admin Panel + +Jede T-SQL-Abfrage wird geloggt. Der Admin kann fehlerhafte Queries per Override korrigieren ohne Code-Deployment. + +```python +class QueryLog(Model): + server = FK(Server) + executed_by = CharField() # "system" / "admin" / username + query_type = CharField(choices=[ + "DISCOVERY", "HEALTH_CHECK", + "DB_CREATE", "DB_DROP", + "LOGIN_CREATE", "LOGIN_DROP", + "AG_CREATE", "AG_DROP", "AG_ADD_DB", "AG_REMOVE_DB", + "QUOTA_CHECK", "PERMISSION_CHECK" + ]) + query_text = TextField() # tatsächlich ausgeführte Query + executed_at = DateTimeField() + duration_ms = IntegerField() + success = BooleanField() + error_message = TextField(null=True) + # Override-System + is_overridden = BooleanField(default=False) + override_query = TextField(null=True) + override_by = FK(User, null=True) + override_at = DateTimeField(null=True) +``` + +**Override-Logik:** Das System prüft vor jeder Ausführung ob eine Override-Query existiert und nutzt diese bevorzugt. Damit kann der Admin z. B. eine fehlerhafte AG-Query reparieren ohne Deployment. + +**API-Endpunkte:** +``` +GET /api/admin/query-logs/ +GET /api/admin/query-logs/?type=AG_CREATE&success=false +PATCH /api/admin/query-logs/{id}/override/ +DELETE /api/admin/query-logs/{id}/override/ +``` + +*** + +## Modul 13 – Quota-Monitoring + +Ein Celery-Beat-Task prüft regelmäßig die tatsächliche Datenbankgröße und vergleicht sie mit dem gebuchten Wert. Warnungen werden geloggt, E-Mails nur wenn `QUOTA_WARNINGS_ENABLED = True`. + +```sql +SELECT + d.name, + SUM(mf.size) * 8 / 1024.0 AS current_size_mb +FROM sys.databases d +JOIN sys.master_files mf ON d.database_id = mf.database_id +WHERE d.name = '{{db_name}}' +GROUP BY d.name; +``` + +```python +class DatabaseQuota(Model): + database = OneToOneField(CustomerDatabase) + booked_size_gb = DecimalField() + current_size_gb = DecimalField() + last_checked = DateTimeField() + warning_threshold_percent = IntegerField(default=80) + critical_threshold_percent = IntegerField(default=95) + last_warning_sent = DateTimeField(null=True) +``` + +*** + +## Modul 14 – E-Mail-System (vorbereitet, deaktiviert) + +```python +# settings.py +EMAIL_NOTIFICATIONS_ENABLED = False +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" +# Aktivierung: EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" + +def send_notification(template, recipient, context): + if not settings.EMAIL_NOTIFICATIONS_ENABLED: + return # Nur loggen, nicht senden +``` + +**Vorbereitete E-Mail-Events:** +- Kundenkonto freigeschaltet / abgelehnt +- Applikation / Datenbank erfolgreich erstellt +- Erstellungsfehler (an Admin + Kunde) +- Löschanfrage Stufe 1 (Bestätigungs-Link) +- Löschung ausgeführt +- Quota-Warnung 80% / 95% +- Service Account Berechtigung fehlt (an Admin) +- Passwort-Reset + +*** + +## Modul 15 – Audit Log + +Das Audit Log protokolliert Benutzeraktionen getrennt vom Query Log. Es ist **read-only** und unveränderbar. + +```python +class AuditLog(Model): + actor = FK(User) + action = CharField(choices=[ + "APP_CREATE", "APP_DELETE_REQUEST", + "APP_DELETE_CONFIRMED", "APP_DELETE_EXECUTED", + "DB_CREATE", "DB_DELETE_REQUEST", + "DB_DELETE_CONFIRMED", "DB_DELETE_EXECUTED", + "LOGIN_CREATE", "LOGIN_DELETE", + "CUSTOMER_APPROVED", "CUSTOMER_REJECTED", + "SERVER_ADDED", "SERVER_REMOVED", + "POOL_CREATED", "AG_CREATED", + "LISTENER_ASSIGNED", "IP_ASSIGNED", + "QUOTA_WARNING", "PERMISSION_FAIL", + "QUERY_OVERRIDE", "EMAIL_SENT" + ]) + target_type = CharField() + target_id = IntegerField() + target_repr = CharField() # Snapshot: "App: MeinShop / customer@example.com" + ip_address = GenericIPAddressField() + timestamp = DateTimeField(auto_now_add=True) + extra_data = JSONField(null=True) +``` + +*** + +## Benötigte Pakete – Vollständige Liste + +### Backend (Python/Django) + +| Paket | Zweck | +| ------------------------------- | ------------------------------------------------ | +| `django` | Web Framework | +| `djangorestframework` | REST API | +| `mssql-django` | Optionaler SQL Server ORM Support | +| `pyodbc` | Verbindung zu Customer SQL Servern | +| `celery` | Async Task Queue | +| `celery[redis]` | Redis als Broker | +| `django-celery-beat` | Scheduled/Periodic Tasks | +| `django-celery-results` | Task-Ergebnisse in DB | +| `djangorestframework-simplejwt` | JWT Authentication | +| `django-cors-headers` | CORS für Next.js | +| `django-filter` | Filterbare API-Endpunkte | +| `drf-spectacular` | OpenAPI / Swagger Dokumentation | +| `cryptography` | Fernet-Verschlüsselung für Service Accounts | +| `netaddr` | IP-Adress- und Netzwerkberechnung | +| `python-decouple` | `.env` Feature-Flags & Config | +| `psycopg2-binary` | mssql Plattform-DB | +| `django-anymail` | E-Mail (vorbereitet, deaktiviert) | +| `django-safedelete` | Soft-Delete Mixin | +| `django-auditlog` | Audit Logging via Signals | +| `Jinja2` | Dynamische Query-Templates (`{{app_name}}` etc.) | +| `redis` | Redis-Client für Cache & Celery | + +### Frontend (Next.js / TypeScript) + +| Paket | Zweck | +|---|---| +| `next-auth` | Authentifizierung + Session | +| `@tanstack/react-query` | Server State & Data Fetching | +| `axios` | HTTP Client | +| `zustand` | State Management | +| `react-hook-form` | Formulare (Buchungs-Wizard, Bestätigungs-Modals) | +| `zod` | Schema-Validierung Frontend + API | +| `shadcn/ui` | UI-Komponentenbibliothek | +| `tailwindcss` | Styling | +| `@tanstack/react-table` | Admin-Tabellen (Server Pool, Query Log, Kunden) | +| `lucide-react` | Icons | +| `sonner` | Toast-Notifications | +| `@codemirror/react` + `@uiw/react-codemirror` | SQL Query-Editor im Admin (Query Override) | +| `date-fns` | Datums-Formatierung | +| `recharts` | Ressourcen-Charts im Admin (CPU/RAM/Disk) | + +*** + +## Ergänzende technische Hinweise + +### Docker Learnings + +- **ODBC Treiber** muss auf dem Django-Server installiert sein: `msodbcsql18` (Linux/Docker) +- **Apt-Key deprecated** in Debian 12+ - moderner GPG-Keyring Approach verwenden +- **Django App Name Conflicts** - Custom `auditlog` app vs `django-auditlog` Package +- **Jinja2 Dependency** - Wird von Django für Templates benötigt, aber oft vergessen +- **Build Cache** - Bei Dependency-Änderungen `--no-cache` verwenden: `docker compose up -d --build --no-cache` + +### Frontend Architektur + +- **Next.js 16 App Router** mit React 19 +- **TanStack Query** für Server State Management +- **Zustand** für Client State +- **BFF Pattern** - API Routes als Proxy zu Django (vermeidet CORS) +- **shadcn/ui + Tailwind** für UI-Komponenten +- **Zod** für Schema-Validierung + +### Testing Strategy + +- **Backend:** pytest mit 99+ Tests (Models, Serializers, Services) +- **Frontend:** Vitest für Unit Tests, Playwright für E2E +- **Coverage Target:** 80%+ für Core Functionality +- **Test Database:** SQLite für schnelle Unit Tests + +### Security + +- **JWT Tokens:** Access 15min, Refresh 7 Tage +- **Fernet Encryption** für Service Account Passwörter +- **Soft Delete** mit django-safedelete (3-stufige Bestätigung) +- **Audit Logging** für alle Benutzeraktionen + +### Bekannte Hürden + +| Problem | Lösung | +|---------|--------| +| apt-key deprecated | GPG Keyring statt apt-key verwenden | +| App Name Konflikt | Nur custom app in INSTALLED_APPS | +| Django Admin 500 | Jinja2 Backend entfernen, nur DjangoTemplates | +| Migration Konflikte | `token_blacklist` aus INSTALLED_APPS entfernen | +| CORS Issues | BFF Pattern mit API Routes | + +*** diff --git a/Notes/Trainings/.DS_Store b/Notes/Trainings/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f7b9b44920d00e70480eeaaeee308686f4d2a5cb GIT binary patch literal 6148 zcmeHKK~BRk5FCdJ6}a@sfrBqxxb+92N>3d70ce9LQbVdD#MPhR1$=;C@B;qBtkvS%ux5)4&lIgS28;n? zV9S7fA3RmCOc)g7r-Mzr0uTo@M`1tys}a2zh-Jc{NDIYjC{aUQx?(sDr`;#NGGS2E zaJaZK=SeHObVG4*b=rM&hbtAWH3p1IinLJdM