Compare commits

..
34 Commits
Author SHA1 Message Date
0qln cd2b6f5c80 add abstract 2026-09-10 19:25:50 +02:00
0qln 983c4f7363 adjust dates 2026-09-10 19:09:35 +02:00
0qln f32af118d6 Zeitraum-Abgrenzung und Manntage-Verteilung ergaenzt
- Abschnitt 'Zeit- und Aufwandsplanung' trennt Vorlauf (Mai/Juni),
  offiziellen Taetigkeitszeitraum (07.07.-25.08.2026) und Nachlauf
- Neuer Unterabschnitt 'Verteilung der Manntage' mit Wochentabelle,
  die die 24 MT ueber 36 Arbeitstage neben dem Tagesgeschaeft ausweist
- Feature-Analyse als Vorlauf gekennzeichnet
- Soll-Ist-Vergleich verweist auf die Abgrenzung
- Gantt-Diagramm mit Meilensteinen fuer Beginn/Ende des Praxiseinsatzes
2026-09-10 19:06:21 +02:00
0qln c6a8d3d828 Diagramme auf Chartgroesse zuschneiden (mmdc --pdfFit); leeren Anhang A.2 entfernt 2026-09-07 23:49:37 +02:00
0qln da3718adae todo 2026-09-07 23:42:43 +02:00
0qln 235b471ad9 Textreduktion 2026-09-07 23:37:09 +02:00
0qln 3ecc28ef73 Aktualisierung auf den aktuellen DevOps-Stand (Abnahme, Bugfix, Handbuch, PROD) 2026-09-07 23:08:06 +02:00
0qln e0cf30bb14 todoa 2026-09-07 22:57:38 +02:00
0qln e6d4fb04a4 Textreduktion: Theorie, Nebenläufigkeit, Tests und Prozessbeschreibung gestrafft
- Newtype/parse-dont-validate auf Projektspezifik gekuerzt
- Nebenlaeufigkeitsszenarien und Gegenmassnahmen als Tabellen statt Fliesstext
- Testcode-Reviewbefunde (Timo Walter) zusammengefasst
- Unicorn-Entwicklungsprozess-Beschreibung gekuerzt (Diagramm traegt Details)
- S3-Infrastrukturbeschaffung: Bewertung entfernt, Text gestrafft
- Normalisierungs-Review (Robin Noack) gekuerzt
2026-09-01 18:20:44 +02:00
0qln a421546df3 5.9.5: IconFile-Werte, Rueckfallverhalten und Symbolverweise ergaenzt 2026-09-01 12:33:54 +02:00
0qln 71c55cb9e8 5.8.4: Freigabe-Endpunkt kommt ohne Speicherzugriff aus 2026-09-01 12:32:30 +02:00
0qln 43feca99a9 5.8: Verweise auf Screenshots der Vorschau und Freigabelinks 2026-09-01 12:30:54 +02:00
0qln 8d251c73a1 5.4.1: Symbolsatz im Anhang, Urheberschaft der Entwuerfe ergaenzt 2026-09-01 12:30:13 +02:00
0qln b38bdca078 4.5.2: Urheberschaft der Loesungsansaetze klargestellt 2026-09-01 12:27:22 +02:00
0qln cdcb38f6d0 Anhang: Screenshots des Dokumentenbereichs; Verweise aus 4.4 2026-09-01 12:26:42 +02:00
0qln f7bff8d238 4.4.2: Zeilendarstellung als Entscheidung von Hanna Ebner ausgewiesen 2026-09-01 12:24:10 +02:00
0qln ffe2483f44 4.4.1: Clickdummy auf die Typfilterung eingegrenzt, Urheberschaft klargestellt 2026-09-01 12:23:36 +02:00
0qln 8ef18a03b2 4.3.3: Verhalten bei unbekannten Typ-Ordnern gegen Code praezisiert 2026-09-01 12:22:39 +02:00
0qln a4452b70b1 Abb. 3.2: Infrastruktur-Chronologie als Flussdiagramm von links nach rechts 2026-09-01 12:06:37 +02:00
0qln b949d1926a 3.4.2: PBI- und Anforderungstabelle einleitend verweisen 2026-09-01 12:04:49 +02:00
0qln 5bc07bd1a9 3.4.1: Unicorn-Softwareprozess statt eigenem Zustandsdiagramm 2026-09-01 12:04:02 +02:00
0qln b6526e76c6 1.3: Systemkontext-Abbildung aus PidI 2 uebernommen 2026-09-01 12:03:03 +02:00
0qln d09434f72f 5.1: Newtype-Pattern und parse-dont-validate als Entwurfsgrundlage 2026-09-01 11:51:10 +02:00
0qln 0b0b89c3cc todos 2026-09-01 11:39:05 +02:00
0qln f83a26a448 add screenshots and reference resources 2026-08-29 21:09:21 +02:00
0qln ce0875f29b chapters: Fliesstext auf 57 reine Textseiten kuerzen
Zwei Kompressionsdurchgaenge ueber Kapitel 2-7. Entfernt wurden
Redundanzen, Meta-Kommentare, Ueberklaerungen und Fuellsaetze;
Fakten, Namen, Daten, Entscheidungen samt Begruendung sowie alle
Abbildungen und Tabellen bleiben unveraendert.

Reine Textseiten: 78 -> 57 (Woerter 19613 -> 12451).
Gesamt-PDF: 138 -> 116 Seiten.
2026-08-25 23:02:17 +02:00
0qln 1a3d0820da main: Taetigkeitszeitraum und Abgabedatum eintragen 2026-08-25 22:32:01 +02:00
0qln bff34658b8 appendix: 23 Code-Ausschnitte aus dem Houston-Repository
Pin nixpkgs to the revision used by itc.componentware: the channel version
ships a latexminted that crashes on Python 3.14, which made every listing
fail and sent latexmk into an endless error prompt. Also force
nonstopmode/halt-on-error so a broken listing can never loop again.
2026-08-25 21:38:22 +02:00
0qln cd3a3e5246 chap 7: Projektabschluss — Soll-Ist, Zielerreichung, Reflexion, Ausblick 2026-08-25 20:50:47 +02:00
0qln 8b43abca8a chap 6: Qualitaetssicherung und Auslieferung; PR-Uebersichtstabelle 2026-08-25 20:45:35 +02:00
0qln f857a244dd chap 5: Umsetzung — 11 Abschnitte + module-components/zip-stream/race-condition diagrams 2026-08-25 20:33:15 +02:00
0qln ce6b01626c adjust todo 2026-08-25 20:25:13 +02:00
0qln ebb506d227 chap 4: Konzeption; lookup trade-off table; longtable fix for runaway float loop 2026-08-25 20:17:44 +02:00
0qln 980e21c252 chap 3: Planung und Vorbereitung; backlog/requirements tables; fix duplicate section headers 2026-08-25 19:56:46 +02:00
136 changed files with 2565 additions and 365 deletions
+3
View File
@@ -2,3 +2,6 @@ $pdf_mode = 4; # lualatex
$bibtex_use = 2; # biber $bibtex_use = 2; # biber
$out_dir = out; $out_dir = out;
$clean_ext .= ' %R.run.xml %R.bbl %R.bcf %R-blx.bib'; $clean_ext .= ' %R.run.xml %R.bbl %R.bcf %R-blx.bib';
# Never stop for a prompt: a failing listing would otherwise loop forever.
$lualatex = 'lualatex -interaction=nonstopmode -halt-on-error %O %S';
Executable
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Zaehlt die reinen Textseiten (Kapitel 1-7 ohne Floats, Frontmatter und Anhang).
set -e
SRC="$(cd "$(dirname "$0")" && pwd)"
WORK=/tmp/pidi3-textcount
rm -rf "$WORK"; mkdir -p "$WORK"
tar -C "$SRC" --exclude=out --exclude=.git -cf - . | tar -C "$WORK" -xf -
cd "$WORK"
# Floats entfernen
for f in $(find chapters -name '*.tex'); do
awk '
/\\begin\{(figure|table|longtable|center)\}/ { d++; next }
/\\end\{(figure|table|longtable|center)\}/ { if (d) { d--; next } }
d == 0 { print }
' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
done
# Nur Kapitel 1-7 bauen
awk '
/\\frontmatterpages/ { next }
/\\appendix/ { print "\\end{document}"; exit }
{ print }
' main.tex > count.tex
sed -i 's/\\printrefs//' count.tex
latexmk -pdflua -interaction=nonstopmode -halt-on-error count.tex >/dev/null 2>&1 || true
pdfinfo out/count.pdf | awk '/Pages/ {print "Reine Textseiten: " $2}'
+6 -2
View File
@@ -29,14 +29,18 @@ and Times Newer Roman (fonts are installed into `~/.local/share/fonts/` by the s
```sh ```sh
# render a single diagram # render a single diagram
mmdc -i figures/diagrams/system-context.mmd -o figures/diagrams/system-context.pdf mmdc -f -i figures/diagrams/system-context.mmd -o figures/diagrams/system-context.pdf
# render all diagrams at once # render all diagrams at once
for f in figures/diagrams/*.mmd; do for f in figures/diagrams/*.mmd; do
mmdc -i "$f" -o "${f%.mmd}.pdf" mmdc -f -i "$f" -o "${f%.mmd}.pdf"
done done
``` ```
`-f` (`--pdfFit`) is mandatory: without it `mmdc` writes the chart onto a full US-Letter
page, so `\includegraphics[width=\textwidth]` scales all that surrounding whitespace up
and a small diagram eats a whole A4 page.
Render before calling `latexmk` whenever a `.mmd` file changes. The generated `.pdf` Render before calling `latexmk` whenever a `.mmd` file changes. The generated `.pdf`
files are gitignored via `out` (only `out/` is ignored, so diagrams in `figures/` are files are gitignored via `out` (only `out/` is ignored, so diagrams in `figures/` are
**not** ignored — commit them). **not** ignored — commit them).
+47
View File
@@ -0,0 +1,47 @@
von mir an den Agenten.
screenshots habe ich in figures/screenshots gelegt.
# Inhaltliche TODOs
- [x] screenshots vom feature in den anhang
- [x] aktualisieren auf den aktuellen devops stand (alles getested, 1 bug fixed, user pdf was documented, prod deployment)
-> "3.5 Zeit- und Aufwandsplanung" aktualisieren
-> "Figure 3.2: Zeitplanung des Projekts" aktualisieren
-> "5.10.5 Stand bei Abgabe" aktualisieren
-> In azure devops, you will see that the last completed (in Test Completed, and in the Deployed in PROD) work item was not "Test Completed" until the 07.09.2026. Pretend that this was completed before the prod release, such that the history looks clean.
-> allgemein ueber alle chapter nochmal drueber gehen und schauen ob das den aktuellen stand reflektiert
- [x] architektur der implementierung:
- ich habe in meiner freizeit sehr viel rust programmiert und dort auch das NewType pattern kennen gelernt, an dem ich mich hier orientierte (auch wenn es C# ist), da ich die vorteile besonders hier wo man eigentlich mit den nur mit ganz vielen string keys rum hantiert, sie aus einander baut, und wieder zusammenbaut, erkannte. id input string -> s3 gibt keys zurueck -> parsing -> domain model -> verbarbeitung -> wenn weitere requests gebraucht werden, vom domain model wieder einen key zusammenbauen
- "parse, don't validate" philosophie
- [x] mit den vom pidi 2 noch relevanten abbildungen ergaenzen
- houston architektur
- unicorn prozess
- etc.
- [x] "Figure 3.1: Zustände eines Work Items in Azure DevOps" sollte mit dem eigentlichem unicorn prozess bild ersaetzt werden. ich arbeite naehmlich nach dem ganz normalen prozess
- "3.4.1 Entwicklungsprozess" sollte dahingehend auch aktualisiert werden, und nicht *Linus-speziefisch* sein.
- [x] "3.4.2 Schnitt der Product Backlog Items": ein abbild aus dem devops oder eine tabelle mit den PBIs ergaenzen verweisen
- [x] "Figure 3.3: Chronologie der Infrastrukturbeschaffung" sieht komisch aus, die pfeile gehen nach unten Chronologie geht aber nach rechts. sollte gefixed werden.
- [x] "4.3.3 Verhalten bei unbekannten Ordnern" (todo fuer mich) ist das richtig? oder werden unbakannte typ-order einfach nicht angezeigt? bin mir da gerade nichtmal sicher
- [x] "4.4.1 Zuarbeit über einen Clickdummy" bezieht sich rein ueber das UI der Typ-Filterung. alles andere stammt von mir. Hanna Ebner hat in Houston die fuehrende Entscheidungskraft, was das UI angeht, und aus dem Grund habe ich mich mit ihr abgestimmt wie das aussehen soll, und sie hat als Referenz den Clickdummy branch erstellt. Die implementierung liegt immer noch in meiner hand. (das ist wichtig fuer das PIDI, da die Arbeit eigentlich von mir kommen soll).
- [x] in "4.4.2 Flache Liste statt navigierbarer Hierarchie": "Die ursprüngliche Beschreibung sah Dokumentkacheln vor, wurde jedoch auf Zeilen-darstellung geändert" auch das ist eine Entscheidung von Hanna Ebner, die meine überwichtete.
- [x] "4.4.3 Aufbau der Seite": screenshots im anhang einfügen und verweisen
- [x] "4.5.2 Untersuchte Lösungsansätze" Lösungsansätze wurden von allein *mir* erarbeitet und mit Timo und Bianco nur abgesprochen und reviewed.
- [x] "5.4.1 Eigene Symbole statt Symbolschrift": icons im anhang einfuegen, darauf hinweisen, dass die Designs von mir entworfen und in Zusammenarbeit mit Hanna Ebner ausgearbeitet wurden.
- [x] "Nicht umgesetzt ist eine Filterung durch den Speicher. Houston listet die Objekte auf und filtert die Namen anschließend selbst. Im Review stellte Hanna Ebner die Frage, ob sich das nicht direkt über die Schnittstelle lösen lasse, und musste verneint werden.": auf den abschnitt verweisen wo die AU die Search Integration abgelehnt hat
- [x] "5.8.3 Vorschau in Messengern": auf screenshots verweisen
- [x] "5.8.4 Sichtbarkeit der Vorschaubilder": darauf verweisen, dass es eigentlich ziemlich elegant ist das gesamte vorschau bild von der base64 encoded id in der url auf dem öffentlichem `/share`-endpunkt zu lesen. somit wird auf diesem öffentlichem endpunkt niemals auch nur ein aufruf nach s3 gemacht.
- [x] "5.9.5 Symbole für Verknüpfungen": screenshots von symbolen verweisen, maybe mehr info zu den icons (siehe resources/xwiki/urlfile_docs.xwiki)
# Strukturelle TODOs
- [x] auf etwa 45 Seiten runter bringen (zuletzt machen nachdem der inhalt angepasst wurde)
- [x] brauchen wir in "3.6 Beschaffung der S3-Infrastruktur" wirklich eine Bewertung...?
- [x] allegemin sowas wie: "Im Review fragte Robin Noack, ob die Einschränkungen ausreichen. Eine zu schwache Norma lisierung führt zu Schlüsseln, die der Speicher zurückweist — das wäre erst bei einem Kunden mit ungewöhnlichem Firmenna men aufgefallen. Die Antwort verwies auf die Herstellerdokumentation (IBM 2026; Amazon Web Services 2026c)." das ist vie lzuviel text einfach nur um zu sagen "laut der IBM doku passt das so"
- [x] "Timo Walter merkte an, dass reale Kundennamen in den Tests verwendet wurden, und schlug Platzhalter vor. Der Hinweis ist klein, in der Sache aber richtig: Testdaten liegen in der Versionsverwaltung und bleiben dauerhaft lesbar; ein realer Kundenname ist eine unnötige Offenlegung, da der Test mit Platzhaltern denselben Zweck erfüllt. Die Daten wur den ersetzt." unnötige details schon wieder...
// ist das noch kaputt? - [ ] Ein paar der bibreferences sehen komisch aus und zeigen nicht richtig auf den korrekten bib eintrag, sollte gefixed werden.
- [x] Die Abbildungen nehmen vertikal sehr viel white space ein, das sollte nicht so sein.
- [ ] ähnlich wie ich es in ~/repos/itc.componentware/ gemacht habe, eine flake und ein build output haben.
# Review
- [x] "4.5 Das Lookup-Problem" auf Inhaltliche korrektheit pruefen
+108 -9
View File
@@ -1,10 +1,109 @@
% TODO: Code-Ausschnitte — je in center-Umgebung mit \captionof{listing}{...} + \inputminted \newcommand{\codelisting}[4]{%
\par\medskip\noindent
\begingroup
\captionsetup{type=listing,hypcap=false}%
\captionof{listing}{#1}%
\label{#2}%
\endgroup
\nopagebreak
\inputminted[
fontsize=\scriptsize,
linenos,
breaklines,
breakanywhere,
breaksymbolleft={},
frame=single,
framesep=1.5mm,
tabsize=4,
numbersep=3mm,
]{#3}{figures/code/#4}%
\par\medskip
}
% Reihenfolge gemäß Kapiteln: Die folgenden Ausschnitte stammen aus dem Houston-Repository und geben den Stand
% s3-settings-registration.cs, s3-documents-client.cs, zum Ende des Projektzeitraums wieder. Sie sind auf die jeweils besprochene Stelle
% documents-page-model.cs, document-view-model.cs, documents-table-body.cshtml, gekürzt; Auslassungen sind mit \texttt{// \ldots} gekennzeichnet.
% document-type-mapping.cs, document-types.resx,
% document-search.cs, pagination-continuation-token.cs, \subsection{Anbindung des Objektspeichers}
% zip-download-action.cs, presigned-url.cs, url-file-parsing.cs,
% org-slug.cs, resolve-customer-prefix.cs, rename-customer-folder.cs, \codelisting{Registrierung des S3-Clients als benannter Dienst, einschließlich der
% authorization-policy.cs, documents-service-tests.cs Korrektur des \texttt{x-amz-copy-source}-Headers}{lst:s3-client-registration}{csharp}{s3-client-registration.cs}
\codelisting{Konfigurationsobjekte für Endpunkt, Zugangsdaten und Bucket}{lst:s3-settings}{csharp}{s3-settings.cs}
\codelisting{Autorisierung des Dokumentenbereichs in \texttt{Program.cs}}{lst:auth-policy}{csharp}{auth-policy.cs}
\clearpage
\subsection{Typisierte Pfade}
\codelisting{\texttt{DocumentKey} — vollständiger Objektschlüssel einschließlich
Kundenordner, nur über eine validierende Fabrikmethode erzeugbar}{lst:document-key}{csharp}{document-key.cs}
\codelisting{\texttt{DocumentId} — die in URLs verwendete Kennung, die den
Kundenordner bewusst nicht enthält}{lst:document-id}{csharp}{document-id.cs}
\codelisting{\texttt{DocumentName} — Ableitung von Anzeigename, PDF- und
Verknüpfungseigenschaft aus der Dateiendung}{lst:document-name}{csharp}{document-name.cs}
\clearpage
\subsection{Dokumenttypen und Symbole}
\codelisting{Dokumenttypen und Ableitung des Ordnernamens aus der
Übersetzungsressource}{lst:document-type}{csharp}{document-type.cs}
\codelisting{Einbindung der Typsymbole als CSS-Maske, damit sie die Themenfarbe
übernehmen}{lst:type-icon-mask}{scss}{type-icon-mask.scss}
\clearpage
\subsection{Auflistung, Suche und Blätterfunktion}
\codelisting{Durchlaufen aller Antwortseiten über den Fortsetzungs-Token}{lst:pagination}{csharp}{pagination-continuation-token.cs}
\codelisting{Auflisten der Dokumente eines Kunden mit Einstiegspunkt für die
Folgeseite}{lst:list-documents}{csharp}{list-documents.cs}
\codelisting{Anwendungsseitige Filterung nach Suchbegriff und
Dokumenttyp}{lst:document-search}{csharp}{document-search.cs}
\codelisting{Tabellenkörper der Dokumentenübersicht}{lst:documents-table}{html}{documents-table-body.cshtml}
\clearpage
\subsection{Download, Vorschau und Freigabe}
\codelisting{Erzeugung vorsignierter URLs und der zugehörigen
\texttt{Content-Disposition}-Kopfzeilen}{lst:presigned-url}{csharp}{presigned-url.cs}
\codelisting{Streamen des ZIP-Archivs ohne Zwischenpufferung}{lst:zip-archive}{csharp}{zip-archive.cs}
\codelisting{Handler des ZIP-Downloads einschließlich der gezielten Freigabe
synchroner Schreibvorgänge}{lst:zip-handler}{csharp}{zip-download-handler.cs}
\codelisting{Anonyme Landeseite für Freigabelinks}{lst:share-page}{csharp}{share-page.cs}
\clearpage
\subsection{Verknüpfungsdateien}
\codelisting{Der eigene INI-Parser}{lst:ini-parser}{csharp}{ini-parser.cs}
\codelisting{Auswertung einer \texttt{.url}-Datei mit Prüfung des
Zielschemas}{lst:url-file}{csharp}{url-file-parsing.cs}
\clearpage
\subsection{Kundenordner}
\codelisting{Bildung des kanonischen Ordnernamens aus dem
Efecte-Firmennamen}{lst:org-slug}{csharp}{org-slug.cs}
\codelisting{Dreistufiger Lookup des Kundenordners}{lst:resolve-customer-prefix}{csharp}{resolve-customer-prefix.cs}
\codelisting{Umbenennen eines Kundenordners — die in Abschnitt~\ref{sec:race-conditions}
beschriebene Stelle}{lst:rename-folder}{csharp}{rename-customer-folder.cs}
\clearpage
\subsection{Tests}
\codelisting{Testdoppel des Objektspeichers: geschriebene Objekte werden für
spätere Abfragen sichtbar}{lst:tests-empty-bucket}{csharp}{tests-empty-bucket.cs}
\codelisting{Test gegen Pfadmanipulation — die Kennung wird bewusst unter Umgehung
der Validierung erzeugt}{lst:test-path-traversal}{csharp}{test-path-traversal.cs}
-1
View File
@@ -1 +0,0 @@
% TODO: eingebundene Diagramme (falls nicht inline in Kapiteln)
+117 -1
View File
@@ -1 +1,117 @@
% TODO: Screenshots der fertigen Documents-Seite (Liste, Filter, Suche, PDF-Modal, ZIP-Auswahl) Die folgenden Aufnahmen zeigen den Dokumentenbereich auf dem Testsystem. Die
Dokumentnamen stammen aus Testdaten und sind bewusst frei erfunden.
\begin{figure}[H]
\centering
\includegraphics[width=\textwidth]{figures/screenshots/explorer_general.png}
\caption{Der Dokumentenbereich in Houston: Suchleiste, Typfilter, Dokumentenliste mit
Mehrfachauswahl und Blätterelemente}
\label{fig:shot-explorer}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=0.85\textwidth]{figures/screenshots/explorer_type-filters.png}
\caption{Typfilterung: Nur die hervorgehobenen Typen werden angezeigt}
\label{fig:shot-type-filters}
\end{figure}
\clearpage
\begin{figure}[H]
\centering
\includegraphics[width=0.85\textwidth]{figures/screenshots/explorer_search-query.png}
\caption{Suche nach einem Namensbestandteil bei gleichzeitig aktiven Typfiltern}
\label{fig:shot-search}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=0.8\textwidth]{figures/screenshots/pagination_page-2.png}
\caption{Zweite Seite der Blätterung; die Adresszeile trägt den Fortsetzungs-Token
\texttt{pageToken} statt einer Seitennummer}
\label{fig:shot-pagination}
\end{figure}
\clearpage
\begin{figure}[H]
\centering
\includegraphics[width=0.8\textwidth]{figures/screenshots/zip-downloads_in-houston.png}
\caption{Auswahl mehrerer Dokumente für den ZIP-Download}
\label{fig:shot-zip-selection}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=0.7\textwidth]{figures/screenshots/zip-downloads_after-download.png}
\caption{Das erzeugte Archiv nach dem Entpacken: Die Typordner sind erhalten,
typlose Dokumente liegen auf oberster Ebene}
\label{fig:shot-zip-result}
\end{figure}
\clearpage
\begin{figure}[H]
\centering
\includegraphics[width=0.85\textwidth]{figures/screenshots/pdf_previewer.png}
\caption{PDF-Vorschau im Modal ohne Verlassen der Seite}
\label{fig:shot-pdf-preview}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=0.8\textwidth]{figures/screenshots/explorer_opened-share-link.png}
\caption{Ergebnis eines geöffneten Freigabelinks: Suche, Typfilter und Seite sind so
gesetzt, dass das verwiesene Dokument sichtbar und hervorgehoben ist}
\label{fig:shot-share-target}
\end{figure}
\clearpage
\begin{figure}[H]
\centering
\includegraphics[width=0.85\textwidth]{figures/screenshots/sharelink-preview_in-teams.png}
\caption{Vorschau eines Freigabelinks in Microsoft Teams mit Dokumentname und Typbild}
\label{fig:shot-share-teams}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=0.85\textwidth]{figures/screenshots/url-file-icons.png}
\caption{Symbole für \texttt{.url}-Dateien: mit ausdrücklichem \texttt{IconFile},
ohne \texttt{IconFile} (Typsymbol des Ordners) sowie bekannte Zielsysteme}
\label{fig:shot-url-icons}
\end{figure}
\begin{figure}[H]
\centering
\newcommand{\typeicon}[1]{\raisebox{-0.35\height}{\includegraphics[height=9mm]{figures/icons/#1.pdf}}}
\begin{tabularx}{\textwidth}{@{} c X c X @{}}
\toprule
\multicolumn{2}{@{}l}{\textbf{Dokumententyp}} & \multicolumn{2}{l@{}}{\textbf{Dokumententyp}} \\
\midrule
\typeicon{service-protokoll} & Service-Protokoll
& \typeicon{security-assessments} & Security Assessments \\
\addlinespace
\typeicon{abnahme-dokumente} & Abnahme-Dokumente
& \typeicon{abrechnungsdaten} & Abrechnungsdaten \\
\addlinespace
\typeicon{sla-reports} & SLA-Reports Ticketbearbeitung
& \typeicon{vertragsunterlagen} & Vertragsunterlagen \\
\addlinespace
\typeicon{monitoring-reports} & Monitoring-Reports
& \typeicon{default} & Sonstige Dokumente (Standardsymbol) \\
\addlinespace
\midrule
\multicolumn{4}{@{}l}{\textbf{Symbole für Verknüpfungen auf bekannte Zielsysteme}} \\
\midrule
\typeicon{ms-teams} & Microsoft Teams
& \typeicon{ms-sharepoint} & Microsoft SharePoint \\
\addlinespace
\typeicon{ms-onedrive} & Microsoft OneDrive & & \\
\bottomrule
\end{tabularx}
\caption{Die für den Dokumentenbereich entworfenen Symbole}
\label{fig:type-icons}
\end{figure}
+153 -2
View File
@@ -1,2 +1,153 @@
% TODO: Zeitplan Soll/Ist, Backlog (PBI-Tabelle), PR-Übersicht, \begin{longtable}{@{} l p{62mm} c c l @{}}
% Anforderungsmatrix, Dokumententypen↔Ordner↔Icon, Lookup-Trade-offs, Beteiligte \caption{Product Backlog Items des Features 484 „Dokumente"}
\label{tab:backlog} \\
\toprule
\textbf{ID} & \textbf{Titel} & \textbf{Aufwand} & \textbf{Sprint} & \textbf{Status} \\
\midrule
\endfirsthead
\toprule
\textbf{ID} & \textbf{Titel} & \textbf{Aufwand} & \textbf{Sprint} & \textbf{Status} \\
\midrule
\endhead
\bottomrule
\endfoot
9295 & Document Explorer & 8 & 15.2026 & Test Completed \\
9549 & Icon Typen UI & 3 & 15.2026 & Test Completed \\
9298 & Suchfunktion im Document Explorer & 3 & 15.2026 & Test Completed \\
9299 & Einzelne Dokumente herunterladen & 3 & 15.2026 & Test Completed \\
9296 & Mehrere Dokumente als ZIP runterladen & 5 & 15.2026 & Test Completed \\
9300 & PDF Modal Previewer & 3 & 15.2026 & Test Completed \\
9294 & Document Link previews UI & 5 & 15.2026 & Test Completed \\
9301 & Support für URL-Dateien & 5 & 15.2026 & Test Completed \\
\addlinespace
9749 & Nach Dokumenttypen filtern & 2 & 16.2026 & Test Completed \\
9731 & Pagination im Document Explorer & 3 & 16.2026 & Test Completed \\
9857 & Recherche: Anfrage Objects nach S3 optimieren & --- & 16.2026 & Done \\
9560 & Automatische Anlage der Ordnerstruktur & 3 & 16.2026 & Test Completed \\
10018 & Kundenordner-Lookup über Efecte-Namen & 3 & 16.2026 & Test Completed \\
\addlinespace
10070 & Race Conditions im Kundenordner-Lookup & \footnotesize Timebox & --- & Approved \\
10134 & \emph{Bug:} Suchfunktion wie auf den anderen Seiten & --- & 17.2026 & Test Completed \\
10167 & Handbuch-Dokumentation erstellen & --- & 17.2026 & Test Completed \\
\end{longtable}
\begin{longtable}{@{} l p{95mm} l @{}}
\caption{Zuordnung der Anforderungen zu den Product Backlog Items}
\label{tab:requirements} \\
\toprule
\textbf{Anf.} & \textbf{Kurzbeschreibung} & \textbf{PBI} \\
\midrule
\endfirsthead
\toprule
\textbf{Anf.} & \textbf{Kurzbeschreibung} & \textbf{PBI} \\
\midrule
\endhead
\bottomrule
\endfoot
FA-1 & Dokumentenliste auf \texttt{/documents} & 9295 \\
FA-2 & Mandantentrennung über \texttt{efecte-org-id} & 9295 \\
FA-3 & Rollenbasierter Zugriff, 403 ohne Berechtigung & 9295 \\
FA-4 & Typisierung und Icons & 9549 \\
FA-5 & Serverseitige Suche nach Titel & 9298 \\
FA-6 & Filter nach Dokumententyp & 9749 \\
FA-7 & Paginierung mit wählbarer Seitengröße & 9731 \\
FA-8 & Einzeldownload über Pre-Signed URL & 9299 \\
FA-9 & ZIP-Download mehrerer Dokumente & 9296 \\
FA-10 & PDF-Vorschau im Modal & 9300 \\
FA-11 & Share-Links mit OpenGraph-Vorschau & 9294 \\
FA-12 & Unterstützung von \texttt{.url}-Dateien & 9301 \\
FA-13 & Automatische Anlage der Ordnerstruktur & 9560 \\
\addlinespace
NFA-1 & Skalierbarkeit des Kundenordner-Lookups & 9857, 10018 \\
NFA-2 & Verständliche Fehlerbehandlung & 9295 \\
NFA-3 & Leerer Zustand bei fehlenden Dokumenten & 9295 \\
NFA-4 & Konsistenz zur bestehenden Oberfläche & 9731, 10134 \\
NFA-5 & Testbarkeit mit gemocktem \texttt{IAmazonS3} & 10018 \\
NFA-6 & Pfadsicherheit beim Download & 9299 \\
\end{longtable}
\begin{longtable}{@{} p{30mm} p{40mm} p{48mm} l @{}}
\caption{Bewertung der Lösungsansätze für den Kundenordner-Lookup}
\label{tab:lookup-tradeoffs} \\
\toprule
\textbf{Ansatz} & \textbf{Vorteil} & \textbf{Ausschlussgrund} & \textbf{Ergebnis} \\
\midrule
\endfirsthead
\toprule
\textbf{Ansatz} & \textbf{Vorteil} & \textbf{Ausschlussgrund} & \textbf{Ergebnis} \\
\midrule
\endhead
\bottomrule
\endfoot
In-Memory-Cache
& Folgeaufrufe kostenlos
& Pro Instanz eigener Cache; nach Neustart leer; linearer Aufwand bleibt
& verworfen \\
\addlinespace
Claim im Anmelde\-token
& Aufwand nur je Anmeldung
& Token unveränderlich, wird bei Umbenennung inkonsistent
& verworfen \\
\addlinespace
Feld in Efecte
& Löst das Problem vollständig
& Zweite Datenhaltung mit Konsistenzrisiko; Nachpflege aller Bestandskunden
& verworfen \\
\addlinespace
S3 Select
& Serverseitige Filterung
& Filtert Objektinhalte, nicht Metadaten mehrerer Objekte
& ungeeignet \\
\addlinespace
StorageGRID Search Integration
& Echte Metadatensuche; fachlich sauberste Lösung
& Vom Betreiber nicht angeboten
& nicht verfügbar \\
\addlinespace
Organisations-ID im Ordnernamen
& Technisch einfachste Lösung
& Zerstört die alphabetische Sortierung nach Kundennamen
& verworfen \\
\addlinespace
\textbf{Ableitung aus dem Firmennamen}
& Ein Aufruf im Regelfall; keine zweite Datenhaltung; selbstheilend
& Mutierender Anteil im Anfragepfad (Abschnitt~\ref{sec:race-conditions})
& \textbf{gewählt} \\
\end{longtable}
\begin{longtable}{@{} l p{48mm} l c c l @{}}
\caption{Pull Requests zum Feature 484}
\label{tab:pull-requests} \\
\toprule
\textbf{PR} & \textbf{Titel} & \textbf{PBI} & \textbf{Laufzeit} & \textbf{Threads} & \textbf{Reviewer} \\
\midrule
\endfirsthead
\toprule
\textbf{PR} & \textbf{Titel} & \textbf{PBI} & \textbf{Laufzeit} & \textbf{Threads} & \textbf{Reviewer} \\
\midrule
\endhead
\bottomrule
\endfoot
2154 & Add document explorer & 9295 & 4 T & 21 & Timo Walter \\
2155 & Add document type icons & 9549 & 16 T & 12 & Timo Walter \\
2156 & Dokumente Suche & 9298 & 16 T & 4 & Timo Walter \\
2157 & Dokument download action & 9299 & 22 T & 30 & Timo Walter \\
2164 & Create document share link previews & 9294 & 22 T & 23 & Timo Walter \\
2169 & Dokumente ZIP Downloads & 9296 & 17 T & 4 & Sarah Hinzmann \\
2170 & Document PDF preview modal & 9300 & 17 T & 2 & Sarah Hinzmann \\
2180 & Document explorer URL file support & 9301 & 9 T & 6 & Robin Noack \\
2186 & Add document type filters & 9749 & 8 T & 5 & Robin Noack \\
2189 & Implement pagination for document explorer & 9731 & 8 T & 5 & Robin Noack \\
2196 & Documents folder management & 9560, 10018 & 9 T & 2 & Robin Noack \\
\addlinespace
2206 & Remove x button for search bar & 10134 & 1 T & 0 & Robin Noack \\
2215 & Update user manual for documents feature & 10167 & 2 T & 0 & Robin Noack \\
\addlinespace
\multicolumn{4}{@{}l}{\textbf{Summe}} & \textbf{114} & \\
\end{longtable}
{\footnotesize
Zusätzlich war die Gruppe \texttt{Development Team} über die Branch-Policy als
Pflicht-Reviewer an allen dreizehn Pull Requests beteiligt. An den Diskussionen der Pull
Requests 2154 bis 2156 beteiligte sich außerdem Hanna Ebner. Alle Pull
Requests wurden freigegeben; keiner wurde abgelehnt oder verworfen.\par}
-5
View File
@@ -3,11 +3,6 @@
\clearpage \clearpage
\section{Diagramme}
\input{appendix/appendix-diagrams}
\clearpage
\section{Screenshots} \section{Screenshots}
\input{appendix/appendix-screenshots} \input{appendix/appendix-screenshots}
+15 -1
View File
@@ -1,4 +1,18 @@
\section{Zielerreichung} \section{Zielerreichung}
\label{sec:evaluation} \label{sec:evaluation}
% TODO \subsection{Bewertung gegen die Zielsituation}
Die in Abschnitt~\ref{sec:target-situation} beschriebene Zielsituation ist erreicht: Kunden verfügen über einen zentralen, selbstständig nutzbaren Zugang zu ihren Dokumenten; die zuvor bestehenden Probleme — fehlende Zentralisierung, kein Self-Service, kein strukturierter Überblick, kein mandantengetrennter Zugriff — sind adressiert. Eine seit Ende 2023 im Backlog liegende Bedarfsnotiz wurde innerhalb eines Projektzeitraums zur produktiven Funktion; entscheidend war die Konkretisierung im Juni, die aus zwei Stichpunkten eine umsetzbare Anforderung machte.
\subsection{Fachliche und technische Ziele}
Sämtliche fachlich geforderten Funktionen sind umgesetzt, über den Kern hinaus auch die Komfortfunktionen Suche, Einzel- und Archivdownload, Vorschau, Freigabelinks und Verknüpfungsdateien. Der Kern — Auflistung mit Mandantentrennung — war nach vier Tagen zusammengeführt, sodass die Folgearbeiten früh beginnen konnten. Die zentrale technische Herausforderung war nicht in der Zielsetzung enthalten, sondern entstand aus der Kombination von Autorisierung über ein Metadatum, lesbaren Ordnernamen und fehlender Metadatensuche des Speichers; die gewählte Lösung erfüllt die Anforderung ohne zweite Datenhaltung, heilt bestehende Abweichungen selbsttätig und ist seit dem 3.~September produktiv.
\subsection{Nicht erreichte Ziele}
Ein Punkt ist offen geblieben, ein weiterer liegt außerhalb des eigenen Einflusses. Die \textbf{Nebenläufigkeit im verändernden Lookup-Pfad} ist analysiert, dokumentiert und mit Lösungsvorschlägen versehen, aber nicht behoben — bewusst abgegrenzt, da die Gegenmaßnahme von einer Rückfrage beim Betreiber abhängt (Abschnitt~\ref{sec:race-conditions}). Die \textbf{Optimierung der Dokumentensuche} bleibt offen, da der Suchdienst des Speicherherstellers nicht bereitsteht; die Auswirkung ist gering, weil die Suche nur mit der Dokumentenzahl eines einzelnen Kunden skaliert.
\subsection{Gesamtbewertung}
Das Projektziel ist erreicht: Der Dokumentenbereich ist seit dem 3.~September vollständig produktiv, erfüllt sämtliche funktionalen Anforderungen, und die verbleibende Arbeit ist klar benannt und im Backlog erfasst. Dass die Nebenläufigkeit durch eigenes Nachprüfen des Codes gefunden wurde, ist dabei bezeichnend (Abschnitt~\ref{sec:reflection}).
+15 -3
View File
@@ -1,6 +1,18 @@
\section{Ausblick} \section{Ausblick}
\label{sec:outlook} \label{sec:outlook}
% TODO: PBI 10070 (Race Conditions), Search Integration (von AU noch nicht verfügbar), \subsection{Absicherung des verändernden Lookup-Pfads}
% Dashboard-Kacheln (Security Assessments, zuletzt hinzugefügte Dokumente),
% Abrechnungsdaten Die in Abschnitt~\ref{sec:race-conditions} beschriebene Nebenläufigkeit ist nach der Produktivsetzung das einzige offene Thema des Features; das Backlog Item ist freigegeben und mit vier bis sechs Stunden veranschlagt. Welche Maßnahme umgesetzt wird, hängt davon ab, ob der Speicher bedingte Schreibvorgänge unterstützt\autocite{aws-conditional-writes}; andernfalls bleiben die Absicherung des Rücknahmeschritts und eine übergreifende Sperre. Alternativ ließe sich der verändernde Anteil aus dem Anfragepfad in einen eigenen, einmalig ausgeführten Vorgang lösen, sodass der Lookup auf reines Lesen zurückgeführt würde.
\subsection{Suchdienst des Speicherherstellers}
Der in Abschnitt~\ref{sec:lookup-research} betrachtete Suchdienst\autocite{storagegrid-search-integration} wurde vom Betreiber nicht bereitgestellt. Sollte er verfügbar werden, ließe sich der Kundenordner-Lookup über eine Metadatenabfrage lösen — die eigene Konstruktion bliebe als Rückfallebene — und die Dokumentensuche serverseitig ausführen. Beides sind Optimierungen; die bestehende Umsetzung funktioniert, skaliert aber schlechter.
\subsection{Fachliche Erweiterungen}
In der Feature-Beschreibung sind zwei nicht in Backlog Items überführte Erweiterungen genannt: die \textbf{Einbindung in die Übersichtsseite} — etwa eine Kachel mit zuletzt hinzugefügten Dokumenten oder Sicherheitsbewertungen, für Letztere wäre ein auswertbares Merkmal pro Dokument nötig, das mit der bestehenden Struktur nicht abbildbar ist — und die \textbf{Bereitstellung von Abrechnungsdaten}, bei der zu klären ist, ob sie über den Dokumentenbereich oder eine gesonderte Ansicht erfolgt, da sie sich in Herkunft und Aktualisierungsrhythmus unterscheiden.
\subsection{Bedienung und Übertragbarkeit}
Die Dokumentenpflege über den externen Dateibrowser war für den Projektzeitraum richtig, aber bewusst als Zwischenlösung getroffen; bei häufigerer Pflege wäre eine Verwaltungsoberfläche im Portal zu bewerten, die die Ordnerstruktur erzwingen könnte. Zwei Bausteine sind wiederverwendbar: der als eigener Dienst gekapselte \textbf{Zugriff auf den Objektspeicher} sowie die \textbf{Muster für Freigabelinks und Vorschaubilder} — Pfadkodierung, eingeschränkte Auslieferung auf einem nicht angemeldeten Endpunkt, Bereitstellung in einem von Kommunikationsanwendungen unterstützten Format.
+23 -2
View File
@@ -1,5 +1,26 @@
\section{Reflexion} \section{Reflexion}
\label{sec:reflection} \label{sec:reflection}
% TODO: Lessons Learned — S3-Lookup-Entscheidung, Race Conditions als offenes Thema, \subsection{Externe Abhängigkeiten früher klären}
% Infrastruktur-Vorlauf unterschätzt (AU-Kommunikation 2026-07-22 bis 2026-08-07)
Zwischen Antrag und abschließender Auskunft über die verfügbaren Speicherfunktionen lagen vier Wochen. Der Fehler lag im Zeitpunkt: Die fachliche Klärung war seit dem 18.~Juni abgeschlossen, der Antrag folgte über vier Wochen später; früher gestellt, hätte die Lookup-Recherche nicht auf eine letztlich negative Antwort warten müssen. Vorgänge, deren Dauer man nicht selbst bestimmt, gehören an den Anfang der Planung.
\subsection{Eine Architekturentscheidung ist die Summe ihrer Randbedingungen}
Das Lookup-Problem war die technisch anspruchsvollste Aufgabe des Projekts. Die zunächst betrachteten Ansätze — Zwischenspeicher, Anmeldetoken, Feld im ITSM-System — wurden verworfen, weil sie jeweils eine zweite Stelle einführen, an der dieselbe Information gepflegt wird. Ausschlaggebend war die Unterscheidung zwischen Anforderung und vermuteter Umsetzung: Die interne Pflege verlangte eine \emph{Sortierung nach Kundennamen}, nicht zwingend Ordnernamen, die \emph{ausschließlich} aus dem Kundennamen bestehen. Ohne diese Rückfrage bei der internen Fachseite wäre der Lösungsraum enger geblieben.
\subsection{Eigene Arbeit gegenlesen}
Die Nebenläufigkeit wurde nicht durch Test, Review oder Fehlerbericht gefunden, sondern durch erneutes Durchgehen des eigenen, fertiggestellten Codes. Die Fehlerbehandlung des Umbenennens war geschrieben worden, um Datenverlust zu verhindern — und genau sie verursacht ihn unter Nebenläufigkeit; ein selbst eingebautes Sicherheitsnetz prüft man nicht ohne Weiteres erneut. Ausschlaggebend war der Perspektivwechsel von „funktioniert das?" zu „was passiert, wenn das zweimal gleichzeitig läuft?" — eine Frage, die für jeden verändernden Codepfad einer Anwendung mit mehreren Instanzen zu stellen ist.
\subsection{Pull Requests kleiner und sequenziell schneiden}
Die in Abschnitt~\ref{sec:code-reviews} beschriebene Ballung war Folge einer im Rückblick falschen Entscheidung: Vier gleichzeitig eröffnete Pull Requests sollten Wartezeiten auf Reviews vermeiden. Der Vorteil trat nicht ein, weil die Arbeiten aufeinander aufbauten — statt Parallelität entstand eine Warteschlange, deren Auflösung sich in die Woche vor der Abnahme verschob; zusätzlich setzte jede Umstellung der verketteten Zielbranches auf den Hauptbranch Freigaben zurück. Bei abhängigen Aufgaben ist sequenzielles Vorgehen vorzuziehen.
\subsection{Konsistenz braucht einen Vergleich, keine Beschreibung}
Der einzige Abnahmefehler betrifft die Anforderung, dass sich die Bedienung an den übrigen Seiten orientieren soll. Sie wurde nicht übersehen, sondern lässt sich nicht aus einer Beschreibung ableiten: Eine Schaltfläche zum Leeren des Suchfelds widerspricht keiner Vorgabe, erst der Vergleich mit den bestehenden Seiten macht die Abweichung sichtbar. Für solche Anforderungen ist ein Abnahmetest durch eine mit der Anwendung vertraute Person unersetzbar und sollte früher angesetzt werden als hier geschehen.
\subsection{Persönliche Einordnung}
Gegenüber den vorangegangenen Praktika lag der Schwerpunkt auf Entwurfsentscheidungen: Die sechs Lösungsansätze für den Lookup und die Nebenläufigkeitsanalyse erzeugten keine einzige Zeile ausgelieferten Codes, bestimmten aber den Projektverlauf. Die Erfahrung, dass eine sauber begründete Abgrenzung mehr wert ist als eine schnelle, unvollständige Behebung, ist die wesentliche Erkenntnis dieses Projekts.
+41 -1
View File
@@ -1,4 +1,44 @@
\section{Soll-Ist-Vergleich} \section{Soll-Ist-Vergleich}
\label{sec:target-comparison} \label{sec:target-comparison}
% TODO: Tabelle Soll-Ist (Zeitplan, Features) \subsection{Zeitlicher Verlauf}
Tabelle~\ref{tab:soll-ist} stellt die geplanten den tatsächlichen Zeitpunkten gegenüber.
\begin{longtable}{@{} p{45mm} l l p{40mm} @{}}
\caption{Soll-Ist-Vergleich der Projektphasen}
\label{tab:soll-ist} \\
\toprule
\textbf{Phase} & \textbf{Soll} & \textbf{Ist} & \textbf{Abweichung} \\
\midrule
\endfirsthead
\toprule
\textbf{Phase} & \textbf{Soll} & \textbf{Ist} & \textbf{Abweichung} \\
\midrule
\endhead
\bottomrule
\endfoot
Feature-Analyse & 18.06. & 18.06. & keine \\
Backlog-Schnitt & 18.06.--08.07. & 18.06.--08.07. & keine \\
Infrastruktur beantragt & --- & 22.07. & nicht eingeplant \\
Infrastruktur verfügbar & --- & 27.07. & nicht eingeplant \\
Umsetzung Sprint 15.2026 & 27.07.--12.08. & 27.07.--19.08. & +1 Woche \\
Umsetzung Sprint 16.2026 & 13.08.--25.08. & 12.08.--26.08. & +1 Tag \\
Abnahmetest & ab 13.08. & 13.08.--25.08. & Zugriffsprobleme \\
Bugfix aus Abnahme (10134) & --- & 27.08. & --- \\
Benutzerhandbuch (10167) & --- & 31.08.--02.09. & nicht eingeplant \\
Produktivsetzung & --- & 03.09. & --- \\
Nebenläufigkeit (10070) & --- & offen & nach Projektzeitraum \\
\end{longtable}
Die Analyse- und Planungsphase verlief planmäßig; die Abweichungen der zweiten Projekthälfte haben zwei Ursachen. Feature-Analyse und Backlog-Schnitt (Juni) sowie Bugfix, Handbuch und Produktivsetzung (ab dem 26.~August) liegen außerhalb des offiziellen Tätigkeitszeitraums vom 07.07.--25.08.2026; die Aufwandsverteilung der 24 Manntage innerhalb dieses Fensters zeigt Tabelle~\ref{tab:manntage}.
\subsection{Ursachen der Abweichungen}
\textbf{Nicht eingeplanter Infrastrukturvorlauf.} Die Beschaffung des Speichers war nicht als eigener Vorgang eingeplant und wurde erst am 22.~Juli beantragt, obwohl die fachliche Klärung seit dem 18.~Juni abgeschlossen war. Die Klärung der Speicherfunktionen zog sich vom 30.~Juli bis zum 17.~August und blockierte währenddessen die Lookup-Recherche (Abschnitt~\ref{sec:reflection}).
\textbf{Nachgeschobene Backlog Items.} Von den 15 Backlog Items waren acht zu Beginn geschnitten; sieben kamen während der Umsetzung hinzu — Typfilter, Paginierung, Recherche zur Speicherabfrage, neuer Lookup, automatische Ordneranlage, Nebenläufigkeit und Benutzerhandbuch. Sie entstanden nicht aus fachlicher Ausweitung, sondern aus technischen Problemen, die erst bei der Implementierung sichtbar wurden; die Schätzung wuchs von 35 auf 46 Aufwandspunkte.
\subsection{Abgleich mit den Anforderungen}
Alle 13 funktionalen Anforderungen sind ausgeliefert, einschließlich der automatischen Anlage der Ordnerstruktur. Bei den nichtfunktionalen Anforderungen sind fünf von sechs erfüllt; nicht erfüllt ist allein die Zusicherung gegen gleichzeitige verändernde Zugriffe. Diese Nebenläufigkeit (Abschnitt~\ref{sec:race-conditions}) ist keine unerfüllte Anforderung, sondern eine neu erkannte Eigenschaft, als eigenes Backlog Item erfasst und freigegeben.
+10 -1
View File
@@ -3,6 +3,15 @@
WorkSimple positioniert sich als Full-Service-IT-Dienstleister, der Unternehmen jeder Größe unterstützt. Die Lösungen sind darauf ausgerichtet, die Komplexität der IT zu reduzieren und gleichzeitig die Effizienz und Sicherheit zu erhöhen. Durch die Nutzung moderner Technologien und agiler Methoden ist WorkSimple in der Lage, schnell und flexibel auf die Anforderungen der Kunden einzugehen. WorkSimple positioniert sich als Full-Service-IT-Dienstleister, der Unternehmen jeder Größe unterstützt. Die Lösungen sind darauf ausgerichtet, die Komplexität der IT zu reduzieren und gleichzeitig die Effizienz und Sicherheit zu erhöhen. Durch die Nutzung moderner Technologien und agiler Methoden ist WorkSimple in der Lage, schnell und flexibel auf die Anforderungen der Kunden einzugehen.
Die intern genutzten Tools wie Kimai (Zeiterfassung) und Efecte (ITSM) sind Beispiele für die praktische Anwendung von IT-Lösungen, die auch Kunden angeboten werden. Dies unterstreicht den praxisnahen Ansatz von WorkSimple. Das Kundenportal Houston, dessen Erweiterung Gegenstand dieses Projekts ist, verbindet diese Systeme gegenüber dem Kunden zu einer einheitlichen Oberfläche — das genaue Zusammenspiel der Kernsysteme wird in Abschnitt~\ref{sec:system-landscape} erläutert. Die intern genutzten Tools wie Kimai (Zeiterfassung) und Efecte (ITSM) sind Beispiele für die praktische Anwendung von IT-Lösungen, die auch Kunden angeboten werden. Dies unterstreicht den praxisnahen Ansatz von WorkSimple. Das Kundenportal Houston, dessen Erweiterung Gegenstand dieses Projekts ist, verbindet diese Systeme gegenüber dem Kunden zu einer einheitlichen Oberfläche.
Abbildung~\ref{fig:houston-uml} zeigt dieses Zusammenspiel im Überblick: Der Kunde besucht Houston und meldet sich über den Azure-Tenant an; Houston bezieht die Sachdaten aus Efecte und Bookstack, während Odoo die Auftragsabwicklung übernimmt. Die Unicorn Development entwickelt Houston und die begleitenden Azure Functions und liefert über Azure DevOps aus. Der Dokumentenbereich ergänzt diese Landschaft um den in Abschnitt~\ref{sec:system-landscape} beschriebenen S3-Speicher.
\begin{figure}[H]
\centering
\includegraphics[width=0.85\textwidth]{figures/houston/uml.png}
\caption{Systemkontext und Zusammenspiel der Kernsysteme zwischen Kunde und WorkSimple}
\label{fig:houston-uml}
\end{figure}
Interne Prozesse und Dokumentation werden im \emph{XWiki} gepflegt, während die Softwareentwicklung nach einem strukturierten DevOps-Prozess mit Azure DevOps erfolgt. Dieser ganzheitliche Ansatz ermöglicht es WorkSimple, sowohl interne als auch kundenseitige IT-Herausforderungen effektiv zu lösen. Interne Prozesse und Dokumentation werden im \emph{XWiki} gepflegt, während die Softwareentwicklung nach einem strukturierten DevOps-Prozess mit Azure DevOps erfolgt. Dieser ganzheitliche Ansatz ermöglicht es WorkSimple, sowohl interne als auch kundenseitige IT-Herausforderungen effektiv zu lösen.
+27 -9
View File
@@ -1,13 +1,31 @@
\section{Autorisierungskonzept} \section{Autorisierungskonzept}
\label{sec:authorization} \label{sec:authorization}
% TODO: App-Rolle Documents.Read (Entra ID), Claim efecte:company_id Das Autorisierungskonzept arbeitet zweistufig: Eine Rollenprüfung entscheidet, \emph{ob} ein Benutzer den Bereich betreten darf, eine Mandantenprüfung, \emph{welche} Dokumente er sieht.
% Menüpunkt nur sichtbar mit Rolle, Direktzugriff ohne Rolle → 403 (nicht 404)
% Sequence-Diagramm auth-sequence.pdf
% \begin{figure}[H] \subsection{Authentifizierung und Claims}
% \centering
% \includegraphics[width=0.9\textwidth]{figures/diagrams/auth-sequence.pdf} Die Authentifizierung erfolgt über Microsoft Entra~ID. Das Token enthält die \textbf{Efecte-Organisations-ID}, über die die Kundenzugehörigkeit bestimmt wird, und den \textbf{Efecte-Firmennamen} aus dem Claim \texttt{efecte:company\_name}. Dass der Firmenname verfügbar ist, wurde erst bei der Lookup-Recherche erkannt — er ermöglicht die Bestimmung des Ordnernamens ohne zusätzlichen Efecte-Aufruf (siehe Abschnitt~\ref{sec:lookup-decision}).
% \caption{Autorisierungsablauf}
% \label{fig:auth-sequence} \subsection{Anwendungsrolle Documents.Read}
% \end{figure}
Für den Zugriff wurde in Entra~ID die Anwendungsrolle \texttt{Documents.Read} angelegt; eine Differenzierung nach Dokumententyp gibt es nicht. Sie steuert die Sichtbarkeit des Navigationspunkts und schützt die Seite gegen direkten URL-Aufruf. Nur die zweite Prüfung ist sicherheitsrelevant; das Ausblenden des Menüpunkts ist Benutzerführung.
\subsection{403 statt 404}
Die ursprüngliche Anforderung sah bei unberechtigtem Zugriff auf \texttt{/documents} eine 404-Antwort vor, um die Existenz der Ressource zu verbergen. Sie wurde auf 403 geändert: Die Existenz des Bereichs ist kein Geheimnis, und 404 erschwert die Fehlersuche, weil ein Benutzer ohne Rolle dieselbe Antwort wie bei einem Tippfehler erhält. Dieser Fall trat später im Abnahmetest ein (siehe Abschnitt~\ref{sec:acceptance-testing}) und bestätigte die Entscheidung.
\subsection{Mandantentrennung}
Aus der Organisations-ID wird der zugehörige Kundenordner aufgelöst, und alle Abfragen werden auf dieses Präfix eingeschränkt (siehe Abschnitt~\ref{sec:s3-layout}). Da die Einschränkung bereits Bestandteil der Abfrage ist, lädt Houston nie Dokumente fremder Kunden, um sie nachträglich herauszufiltern; ein Fehler in der Darstellungsschicht kann somit nicht zu einer Offenlegung führen. Abbildung~\ref{fig:auth-sequence} stellt den Ablauf dar.
\begin{figure}[H]
\centering
\includegraphics[width=\textwidth]{figures/diagrams/auth-sequence.pdf}
\caption{Ablauf von Authentifizierung und Autorisierung}
\label{fig:auth-sequence}
\end{figure}
\subsection{Absicherung der Einzelzugriffe}
Auch Download, ZIP-Download, PDF-Vorschau und Freigabelinks nehmen einen Dokumentschlüssel entgegen. Für jeden Pfad wird der Schlüssel gegen das aufgelöste Kundenpräfix geprüft und auf Bestandteile untersucht, mit denen sich der Kundenordner verlassen ließe (NFA-6). Freigabelinks bilden keine Ausnahme: Sie enthalten keine Anmeldeinformationen; ein Empfänger ohne Rolle und passende Organisationszugehörigkeit erhält dieselbe 403-Antwort.
+36 -4
View File
@@ -1,7 +1,39 @@
\section{Dokumententypen und Icons} \section{Dokumententypen und Icons}
\label{sec:document-types} \label{sec:document-types}
% TODO: 7 Typen hardcoded in Houston (PBI 9560/9549): \subsection{Der Typenkatalog}
% Service-Protokoll, Abnahme Dokumente, SLA-Reports, Monitoring Reports,
% Security Assessments, Abrechnungsdaten, Vertragsunterlagen Die sieben Dokumententypen wurden in der Feature-Beschreibung festgelegt und im Projektverlauf nicht verändert. Tabelle~\ref{tab:document-types} zeigt den Katalog mit dem jeweiligen Ordnernamen im Speicher.
% Typzuordnung über Unterordnername, Tabelle im Anhang
\begin{table}[H]
\centering
\begin{tabularx}{\textwidth}{@{} l X @{}}
\toprule
\textbf{Ordnername im S3} & \textbf{Fachliche Bedeutung} \\
\midrule
\texttt{Service-Protokoll} & Protokolle erbrachter Serviceleistungen \\
\texttt{Abnahme Dokumente} & Abnahmeprotokolle abgeschlossener Projekte \\
\texttt{SLA-Reports Ticketbearbeitung} & Auswertungen zur Einhaltung vereinbarter Reaktionszeiten \\
\texttt{Monitoring Reports} & Periodische Auswertungen aus der Systemüberwachung \\
\texttt{Security Assessments} & Ergebnisse von Sicherheitsbewertungen \\
\texttt{Abrechnungsdaten} & Abrechnungsunterlagen, gegliedert nach Monat, Jahr und Service \\
\texttt{Vertragsunterlagen} & Verträge, Leistungsscheine und zugehörige Dokumente \\
\bottomrule
\end{tabularx}
\caption{Katalog der Dokumententypen}
\label{tab:document-types}
\end{table}
\subsection{Feste Kodierung statt Konfiguration}
Im Approval-Termin stellte \emph{Timo Walter} die Frage, ob die Kategorien konfigurierbar sein sollen. Die Entscheidung fiel auf eine feste Kodierung in Houston. Der Typenkatalog ist fachlich stabil und ändert sich allenfalls im Rhythmus von Jahren; Konfigurierbarkeit würde Verwaltungsoberfläche, Speicherformat und Migrationsstrategie erfordern. Da jeder Typ zudem ein Icon und eine Übersetzung benötigt, stellt die feste Kodierung sicher, dass Ordnername, Anzeigetext und Icon gemeinsam gepflegt werden. Der Preis — eine Codeänderung und ein Release je neuem Typ — ist bei der erwarteten Änderungshäufigkeit vertretbar.
\subsection{Verhalten bei unbekannten Ordnern}
Unterordner, die nicht im Katalog stehen — etwa ein in Filestash von Hand angelegtes \texttt{Sonstiges} — werden angezeigt: Die Typableitung liefert keinen Treffer, das Dokument gilt als typlos und erhält den Anzeigetext \emph{Sonstige Dokumente} sowie ein neutrales Standard-Icon. Ausblenden wurde verworfen, weil es stilles Fehlverhalten erzeugen würde. Dieselbe Behandlung greift für Dokumente unmittelbar im Kundenordner. Praktisch bleibt der Fall die Ausnahme, da Houston die Ordnerstruktur selbst anlegt (siehe Abschnitt~\ref{sec:folder-management}) und dabei nur Katalog-Ordner erzeugt.
Zu unterscheiden ist dies vom Umgang mit unbekannten Ordnern auf der \emph{obersten} Ebene, wo ein fehlendes Marker-Metadatum zum vollständigen Ausschluss führt (siehe Abschnitt~\ref{sec:s3-layout}): Dort entscheidet die Struktur über die Mandantentrennung, während innerhalb eines Kundenordners die Zugehörigkeit bereits geklärt ist.
\subsection{Übersetzung der Anzeigenamen}
Die Ordnernamen müssen für die Kundenbetreuer in Filestash lesbar und eindeutig sein, für den Kunden in Houston aber in die Oberfläche passen. Der Ordnername wird daher über eine Ressourcendatei auf einen Anzeigetext abgebildet — das etablierte Lokalisierungsmuster. So lässt sich der Anzeigetext ändern, ohne die Speicherstruktur anzufassen, deren Umbenennung das Umkopieren sämtlicher betroffenen Objekte erfordern würde.
+38 -10
View File
@@ -1,13 +1,41 @@
\section{Architekturentscheidung: O(1)-Fast-Path} \section{Architekturentscheidung: Namensgebung durch Houston}
\label{sec:lookup-decision} \label{sec:lookup-decision}
% TODO: PBI 10018 — metadatenbasierter Fast-Path \subsection{Die zugrunde liegende Idee}
% ResolveCustomerPrefixAsync: Fast-Path → Kollisions-Fast-Path → O(n)-Fallback + Self-Healing-Rename
% Houston als führende Instanz für Ordnerbenennung
% \begin{figure}[H] Alle in Abschnitt~\ref{sec:lookup-research} betrachteten Ansätze hinterlegen die Zuordnung \emph{zusätzlich} irgendwo und führen so eine zweite, konsistent zu haltende Datenhaltung ein. Die gewählte Lösung dreht die Fragestellung um: Lässt sich der Ordnername deterministisch aus dem Firmennamen ableiten und steht dieser im Token, bestimmt Houston ihn ohne Nachschlagevorgang — aus der Suche wird ein direkter Zugriff.
% \centering
% \includegraphics[width=\textwidth]{figures/diagrams/lookup-flow.pdf} Voraussetzung ist, dass die Ordner so heißen, wie Houston es erwartet. \textbf{Houston wird zur führenden Instanz für die Namensgebung der Kundenordner}; weicht ein Ordner ab, wird er umbenannt. Das ist vertretbar, weil Houston die Ordnerstruktur ohnehin selbst anlegt (FA-13), und die Kundenbetreuer verlieren nichts: Der Ordner heißt weiterhin nach dem Kunden, nur normalisiert.
% \caption{Ablauf \texttt{ResolveCustomerPrefixAsync}}
% \label{fig:lookup-flow} \subsection{Ableitung des Ordnernamens}
% \end{figure}
Der Firmenname aus dem Token kann problematische Zeichen, führende Leerzeichen oder beliebige Länge aufweisen und wird daher durch eine Normalisierungsfunktion geführt, im Folgenden \emph{Slug} genannt. Sie muss \textbf{deterministisch} und \textbf{lesbar} sein, da der Ordner in Filestash von Menschen bedient wird. Umlaute werden bewusst \emph{nicht} ersetzt: „Müller GmbH" erhält den Ordner \texttt{Müller GmbH}, nicht \texttt{Mueller GmbH}, sonst stünde die Firma in der alphabetischen Liste an unerwarteter Stelle. Die Zulässigkeit solcher Zeichen in Objektschlüsseln wurde anhand der Herstellerdokumentation geprüft \autocite{aws-s3-naming, ibm-s3-naming}.
\subsection{Umgang mit Namenskollisionen}
Firmennamen sind nicht garantiert eindeutig: Efecte lässt zwei Organisationen mit identischem Namen zu, die auf denselben Ordnernamen träfen — eine Verwechslung würde bedeuten, dass ein Kunde fremde Dokumente sieht. Die erste Organisation, die den Namen beansprucht, erhält ihn; jede weitere erhält einen Ordner mit angehängter Organisations-ID, etwa \texttt{Beispielkunde GmbH (77)}. Ein Zielname wird nur beansprucht, wenn er frei ist oder ausweislich seines Markers der eigenen Organisation gehört; die Zuordnung entscheidet immer das Metadatum, nie der Name.
\subsection{Der resultierende Ablauf}
Aus diesen Überlegungen ergibt sich ein dreistufiges Verfahren (Abbildung~\ref{fig:lookup-flow}).
\begin{figure}[H]
\centering
\includegraphics[width=\textwidth]{figures/diagrams/lookup-flow.pdf}
\caption{Auflösung des Kundenordners}
\label{fig:lookup-flow}
\end{figure}
\begin{enumerate}
\item \textbf{Direktzugriff.} Houston bildet den erwarteten Ordnernamen aus dem Firmennamen und ruft die Metadaten des Markers ab. Stimmt die Organisations-ID, ist die Auflösung mit einem Aufruf abgeschlossen — der Regelfall.
\item \textbf{Kollisionsprüfung.} Fehlt der Marker oder trägt er eine fremde ID, wird der Zugriff mit dem um die Organisations-ID ergänzten Namen wiederholt — zwei Aufrufe.
\item \textbf{Rückfallebene.} Erst dann kommt das lineare Verfahren zum Einsatz. Ein Ordner mit passender ID wird auf den kanonischen Namen umbenannt; wird keiner gefunden, wird die vollständige Struktur angelegt.
\end{enumerate}
\subsection{Selbstheilung}
Die Rückfallebene beseitigt nicht nur den Fehlschlag, sondern dessen Ursache: Nach der Umbenennung trägt der Ordner den erwarteten Namen, und der nächste Zugriff läuft direkt. Der teure Pfad wird pro Kunde höchstens einmal durchlaufen, Bestandsordner migrieren sich beim ersten Zugriff von selbst. Das Verfahren erfordert keine zusätzliche Datenhaltung; der Zustand liegt vollständig im Speicher.
\subsection{Bewusst offen gelassener Bereich}
Die Rückfallebene benennt Ordner um und legt sie an — im Rahmen einer gewöhnlichen Seitenanfrage. Da Houston in mehreren Instanzen läuft, können zwei Anfragen gleichzeitig denselben Ordner betreffen. Dieser Umstand wurde als eigenes Backlog Item vom laufenden Pull Request abgegrenzt; Abschnitt~\ref{sec:race-conditions} behandelt die Szenarien und Gegenmaßnahmen.
+21 -5
View File
@@ -1,8 +1,24 @@
\section{Problem: Lookup des Kundenordners} \section{Das Lookup-Problem}
\label{sec:lookup-research} \label{sec:lookup-research}
% TODO: PBI 9857 — S3 kann nicht nach Metadaten-Tags suchen → lineares Scannen aller Top-Level-Prefixes \subsection{Entstehung}
% Trade-off-Matrix der 6 Varianten (In-Memory Cache, Claim, Efecte-Feld, S3 SelectObject, Search Integration, Prefix im Namen)
% \cite{aws-s3-select}, \cite{storagegrid-search-integration}
% Tabelle~\ref{tab:lookup-tradeoffs} im Anhang Der Document Explorer war zu seinem Merge am 31.~Juli 2026 funktionsfähig, enthielt aber eine im Code-Review angesprochene Schwäche: Um den Kundenordner zu einer Organisations-ID zu finden, listete Houston sämtliche Ordner auf oberster Ebene auf, rief für jeden die Metadaten ab und verglich die \texttt{efecte-org-id} mit der des Benutzers. Ursache ist, dass S3 keine Suche nach benutzerdefinierten Metadaten bietet: \texttt{ListObjectsV2} liefert Schlüssel, Größe und Änderungszeitpunkt, aber keine Metadaten \autocite{aws-listobjectsv2}, sodass jeder Kandidat einen eigenen Aufruf erfordert.
Der Aufwand wächst linear mit der Kundenzahl — bei \emph{jedem} Seitenaufruf, also einige hundert Netzwerkaufrufe bei einigen hundert Kunden, bevor das erste Dokument geladen wird. Das Problem ist nicht akut, aber strukturell. Am 28.~Juli entstand daraus ein eigenes Backlog Item, bewusst als \emph{Recherche}-Item mit dem Akzeptanzkriterium „es ist sich für einen Lösungsansatz entschieden worden", um den Rechercheaufwand als eigenständige Leistung sichtbar zu machen.
\subsection{Untersuchte Lösungsansätze}
Die sechs folgenden Ansätze habe ich im Rahmen des Recherche-Items selbst erarbeitet und abgewogen; \emph{Timo Walter} und \emph{Bianco Veigel} dienten als Gesprächspartner und prüften das Ergebnis im Review. Tabelle~\ref{tab:lookup-tradeoffs} im Anhang fasst die Bewertung zusammen.
\textbf{In-Memory-Cache.} Das Auflösungsergebnis zwischenspeichern macht Folgeaufrufe kostenlos. Da Houston in mehreren Instanzen läuft und der Cache nach jedem Neustart leer ist, bleibt der lineare Aufwand jedoch bestehen, er wird nur seltener bezahlt.
\textbf{Speicherung im Anmeldetoken.} Der Ordnername könnte als Claim abgelegt werden. Der lineare Aufwand bliebe erhalten, und ein Token ist über seine Laufzeit unveränderlich: Nach einer Umbenennung zeigt der Claim bis zur Erneuerung auf einen nicht mehr existierenden Ordner.
\textbf{Speicherung als Feld in Efecte.} Der Ordnername könnte an der Organisation in Efecte gepflegt werden. Das löst das Problem technisch, führt aber eine zweite Datenhaltung neben dem Marker-Metadatum ein, die auseinanderlaufen kann, und müsste für jeden Bestandskunden nachgepflegt werden.
\textbf{S3 Select.} \texttt{SelectObjectContent} filtert den \emph{Inhalt} eines Objekts serverseitig per SQL-ähnlicher Abfrage \autocite{aws-s3-select}; denkbar wäre eine Zuordnungstabelle als Datei. Die Freischaltung wurde am 7.~August bestätigt (siehe Abschnitt~\ref{sec:infrastructure}), doch S3 Select filtert Inhalte einzelner Objekte, nicht Metadaten mehrerer — die Zuordnungsdatei hätte dasselbe Konsistenzproblem wie das Efecte-Feld ohne dessen Werkzeugunterstützung.
\textbf{StorageGRID Search Integration.} Der Search Integration Service spiegelt Objektmetadaten in einen Elasticsearch-Index und ermöglicht eine echte Metadatensuche \autocite{storagegrid-search-integration} — fachlich die sauberste Lösung. Die Anfrage ergab am 17.~August, dass die Funktion derzeit nicht angeboten wird; der Ansatz wurde in den Ausblick übernommen (siehe Abschnitt~\ref{sec:outlook}).
\textbf{Organisations-ID im Ordnernamen.} Die ID zum Bestandteil des Ordnernamens machen — etwa \texttt{42\_Beispielkunde GmbH} — reduzierte den Lookup auf eine Präfixabfrage. In der Abstimmung mit \emph{Ralf Schulte} wurde deutlich, dass die Kundenbetreuer die Ordner in Filestash nach Kundennamen sortiert vorfinden müssen; ein vorangestellter Zahlenschlüssel zerstört diese Sortierung. Die entscheidende Beobachtung war, dass die Anforderung eine Sortierung nach Kundennamen verlangt — nicht, dass der Ordnername \emph{ausschließlich} aus dem Kundennamen besteht. Das eröffnete den Weg zur in Abschnitt~\ref{sec:lookup-decision} beschriebenen Lösung.
+41 -10
View File
@@ -1,14 +1,45 @@
\section{Ablagekonzept im S3-Speicher} \section{Ablagekonzept im S3-Speicher}
\label{sec:s3-layout} \label{sec:s3-layout}
% TODO: Ordnerbaum (Feature 484): Das Ablagekonzept muss die Zuordnung von Dokumenten zu Kunden und die Abbildung des fachlichen Dokumententyps leisten — beides ausschließlich über die Struktur im Speicher, ohne zusätzliche Datenbank.
% Bucket → Kundenordner [meta: efecte_org-id] → Typordner/ → Dokumente
% Typ = Unterordner, nicht Metadatum an der Datei
% Leere Ordner werden nicht angezeigt
% \begin{figure}[H] \subsection{Präfixe statt Ordner}
% \centering
% \includegraphics[width=0.75\textwidth]{figures/diagrams/s3-layout.pdf} S3 adressiert jedes Objekt über einen Schlüssel in einem flachen Namensraum; die scheinbare Hierarchie entsteht beim Auflisten über Präfix und Trennzeichen \autocite{aws-listobjectsv2}. Ein Schlüssel wie
% \caption{S3-Ablagestruktur}
% \label{fig:s3-layout} \begin{quote}
% \end{figure} \texttt{Beispielkunde GmbH/Service-Protokoll/Protokoll-2026-07.pdf}
\end{quote}
erscheint in Filestash als zweistufige Ordnerhierarchie, ist im Speicher aber nur eine Zeichenkette. Ein „Ordner" existiert erst mit einem Objekt unter dem Präfix; ein leerer Ordner lässt sich nur durch ein Platzhalterobjekt simulieren, dessen Schlüssel auf das Trennzeichen endet. Abbildung~\ref{fig:s3-layout} zeigt die Struktur.
\begin{figure}[H]
\centering
\includegraphics[width=0.85\textwidth]{figures/diagrams/s3-layout.pdf}
\caption{Ablagestruktur im S3-Speicher}
\label{fig:s3-layout}
\end{figure}
\subsection{Kundenzuordnung über ein Marker-Objekt}
Auf der obersten Ebene liegt für jeden Kunden ein Ordner. Dessen Platzhalterobjekt trägt ein benutzerdefiniertes Metadatum \texttt{efecte-org-id} und wird im Folgenden \emph{Marker} genannt. Er sorgt dafür, dass der Kundenordner auch ohne Dokumente existiert, und trägt die Organisationszugehörigkeit an genau einer Stelle.
Die Organisations-ID wird als Metadatum statt als Bestandteil des Ordnernamens geführt (siehe Abschnitt~\ref{sec:feature-analysis}): Die Ordner werden von Kundenbetreuern über Filestash gepflegt, ein Name wie \texttt{42} wäre dort schwer handhabbar. Genau diese Entscheidung erzeugt das zentrale technische Problem des Projekts (Abschnitt~\ref{sec:lookup-research}): Houston kennt die Organisations-ID, muss daraus den Ordnernamen ermitteln — und S3 bietet keine Suche nach Metadaten.
\subsection{Typisierung über Unterordner}
Innerhalb des Kundenordners liegt für jeden der sieben Dokumententypen ein Unterordner; der Typ ergibt sich aus dem Unterordner, direkt im Kundenordner liegende Dokumente gelten als untypisiert. Die Alternative — den Typ als Metadatum an jeder Datei zu hinterlegen — wurde verworfen: Ein Metadatum müsste bei jedem Upload manuell gesetzt werden, wofür Filestash keine komfortable Unterstützung bietet, während das Ablegen in einem Ordner die natürliche Bedienhandlung ist. Zudem ist der Typ als Präfixbestandteil unmittelbar aus dem Schlüssel ablesbar und serverseitig filterbar, während ein Metadatum für jedes Objekt einzeln abgerufen werden müsste, da \texttt{ListObjectsV2} es nicht mitliefert — bei $n$ Dokumenten also $n$ zusätzliche Aufrufe.
Dem stehen zwei bewusst in Kauf genommene Nachteile gegenüber: Ein Dokument kann nur einen Typ haben, und die Ordner müssen existieren — die automatische Anlage wird damit zu einer eigenen Anforderung (FA-13).
\subsection{Sichtbarkeitsregeln}
Für die Darstellung gelten drei Regeln:
\begin{enumerate}
\item \textbf{Ordner werden nicht als Einträge angezeigt.} Die Liste zeigt ausschließlich Dokumente; die Ordnerstruktur wird über Typ-Icons und Filter abgebildet.
\item \textbf{Leere Typordner erscheinen nicht.}
\item \textbf{Ordner ohne gültiges Marker-Metadatum werden ignoriert.} Fehlt die Organisations-ID, bleibt der Ordner unsichtbar; der Fall wird protokolliert, führt aber nicht zu einem Fehler.
\end{enumerate}
Die dritte Regel ist eine Sicherheitsmaßnahme: Ein fehlendes Metadatum führt zum Ausschluss, nicht zu einer Vermutung.
+30 -3
View File
@@ -1,5 +1,32 @@
\section{UI-Konzept und Clickdummy} \section{UI-Konzept}
\label{sec:ui-concept} \label{sec:ui-concept}
% TODO: Hanna Ebner, Branch Clickdummy_Dokumente (2026-07-22) \subsection{Abstimmung der Typfilterung über einen Clickdummy}
% Listenansicht, Typfilter oben als Buttons, flexible Tabellenspalten
Das Oberflächenkonzept stammt bis auf einen Punkt aus der eigenen Ausarbeitung: Für die Typfilterung existierte kein Vorbild in Houston, und da \emph{Hanna Ebner} die führende Entscheidungsinstanz für die Oberfläche ist, wurde die Filterleiste mit ihr abgestimmt. Als Referenz setzte sie ihren Vorschlag am 22.~Juli 2026 als lauffähigen Clickdummy in einem eigenen Branch des Houston-Repositories um und verlinkte ihn am Feature. Er ist eine gestalterische Vorgabe für ein einzelnes Bedienelement, keine Zuarbeit zur Umsetzung: Die Implementierung der Filterleiste — Markup, Zustandshaltung, Abfrageparameter und serverseitige Auswertung (Abschnitt~\ref{sec:filter-pagination}) — lag wie die des übrigen Dokumentenbereichs vollständig in meiner Hand.
\subsection{Flache Liste statt navigierbarer Hierarchie}
Der Dokumentenbereich stellt trotz der Bezeichnung „Document Explorer" keine navigierbare Ordnerhierarchie dar; der Benutzer sieht eine flache Liste aller Dokumente, die Typzugehörigkeit wird über Icons und Filter ausgedrückt. Bei einer Hierarchie müsste ein Kunde die Kategorie kennen und sich dorthin durchklicken, während die flache Liste unmittelbares Suchen und Filtern erlaubt. Bei nur zwei Ebenen mit sieben festen Kategorien stünde der Navigationsaufwand in keinem Verhältnis zum Nutzen.
Die ursprüngliche Feature-Beschreibung sah Dokumentkacheln vor. Die Darstellung wurde auf Zeilen umgestellt — eine Entscheidung von \emph{Hanna Ebner}, die meine abweichende Einschätzung überwog und sich als tragfähiger erwies: Eine Zeile bietet Platz für Icon, Name, Auswahlbox und Aktionsschaltflächen, fügt sich in die Tabellendarstellung der übrigen Houston-Seiten ein (NFA-4) und lässt sich um Spalten erweitern, ohne den Aufbau zu verändern.
\subsection{Aufbau der Seite}
Die Seite gliedert sich von oben nach unten in vier Bereiche (Abbildung~\ref{fig:shot-explorer} im Anhang):
\begin{enumerate}
\item Eine \textbf{Suchleiste} am oberen Rand, über die nach dem Dokumentnamen gesucht wird.
\item Darunter \textbf{Filterelemente}, je eines pro Dokumententyp, mit denen sich Typen ein- und ausblenden lassen.
\item Die \textbf{Dokumentenliste} als Tabelle. Jede Zeile enthält Typ-Icon, Dokumentnamen sowie die Aktionen Herunterladen, Teilen und — bei PDF-Dateien — Vorschau. Eine Auswahlbox am Zeilenanfang dient der Mehrfachauswahl für den ZIP-Download.
\item Am unteren Rand die \textbf{Blätterelemente} zum Seitenwechsel sowie die Auswahl der Seitengröße.
\end{enumerate}
Suche und Typfilter wirken zusammen und schränken die Liste gemeinsam ein
(Abbildungen~\ref{fig:shot-type-filters} und~\ref{fig:shot-search}).
\subsection{Konsistenz zur bestehenden Anwendung}
Eine durchgängige Vorgabe war, dass sich der Dokumentenbereich wie die übrigen Houston-Seiten bedienen lässt (NFA-4). Wie genau das zu verstehen ist, zeigte sich erst im Abnahmetest: Die Suchleiste blendete nach einer Eingabe eine Schaltfläche zum Leeren ein — eine Funktion, die es auf den übrigen Seiten nicht gibt. Der Unterschied wurde als Fehler gemeldet (siehe Abschnitt~\ref{sec:acceptance-testing}) und verdeutlicht, dass eine Konsistenzanforderung am Vergleich mit dem Bestand geprüft werden muss.
Ein Freigabelink auf ein bestimmtes Dokument muss auch funktionieren, wenn dieses nicht auf der ersten Seite liegt. Der Link setzt daher beim Weiterleiten die passenden Abfrageparameter, sodass die richtige Seite geladen und an die entsprechende Stelle gesprungen wird (Abbildung~\ref{fig:shot-share-target} im Anhang).
+27 -9
View File
@@ -1,12 +1,30 @@
\section{Architektur des Documents-Moduls} \section{Architektur des Dokumentenmoduls}
\label{sec:architecture} \label{sec:architecture}
% TODO: Razor Page DocumentsPage.cshtml → DocumentsService → S3DocumentsClient \subsection{Schichtung}
% Klassendiagramm module-components.pdf
% \begin{figure}[H] Das Dokumentenmodul folgt der in Houston etablierten Schichtung (Abbildung~\ref{fig:module-components}).
% \centering
% \includegraphics[width=0.9\textwidth]{figures/diagrams/module-components.pdf} \begin{figure}[H]
% \caption{Komponentenstruktur des Documents-Moduls} \centering
% \label{fig:module-components} \includegraphics[width=0.95\textwidth]{figures/diagrams/module-components.pdf}
% \end{figure} \caption{Komponenten des Dokumentenmoduls}
\label{fig:module-components}
\end{figure}
Die oberste Schicht bilden drei Razor Pages: \texttt{Documents} für Liste, Suche, Filter und Blätterelemente, \texttt{Document/Download} für Downloads (einzeln und als ZIP) und \texttt{Document/Share} für Freigabelinks. Darunter liegt der \texttt{DocumentsService} als fachliche Schicht mit den Regeln des Ablagekonzepts (Kundenordner-Auflösung, Typableitung, Ausblendung technischer Einträge). Der \texttt{S3DocumentsClient} kapselt den Speicherzugriff und ist die einzige Stelle, an der \texttt{IAmazonS3} unmittelbar verwendet wird.
\subsection{Eigene Typen statt Zeichenketten}
\label{sec:newtype}
Anfangs wurden Dokumentschlüssel als Zeichenketten durch die Schichten gereicht. Im Review des Downloads führte das zu wiederholten Rückfragen zur Pfadvalidierung, weil einer Zeichenkette nicht anzusehen ist, ob sie bereits geprüft wurde. Als Ausweg verpackte ich die Pfade nach dem \emph{Newtype-Pattern} in eigene \texttt{readonly record struct}-Typen \autocite{rust-newtype}, ergänzt um „parse, don’t validate“ \autocite{king-parse}: Wer einen \texttt{DocumentKey} entgegennimmt, muss die Gültigkeit nicht erneut prüfen.
So entstanden \texttt{DocumentKey} für den vollständigen Pfad einschließlich Kundenordner (Listing~\ref{lst:document-key}) und \texttt{DocumentRelativePath} für den Anteil darunter, beide nur über eine prüfende Fabrikmethode erzeugbar; eine vergessene Prüfung führt so zu einem Übersetzungsfehler statt zu einer Sicherheitslücke. Nach demselben Muster entstanden \texttt{DocumentType}, \texttt{DocumentName}, \texttt{DocumentId} und \texttt{OrgSlug}. Zeichenketten werden einmal am Rand in diese Typen geparst; die weitere Verarbeitung arbeitet nur noch auf Typen. Der zugehörige Pull Request durchlief 23 Iterationen und bestand zu einem erheblichen Teil aus diesem Refactoring, das sich auszahlte, weil ZIP-Download, PDF-Vorschau und Freigabelinks dieselben Typen wiederverwenden konnten.
\subsection{Zentrale Autorisierung}
Die erste Fassung des Document Explorers prüfte die Berechtigung im PageModel. Im Review wies \emph{Hanna Ebner} darauf hin, dass die Autorisierung in Houston zentral in der Anwendungskonfiguration erfolgt und eine zusätzliche Prüfung an der Seite überflüssig ist. Eine seitenspezifische Prüfung ist zudem leicht zu übersehen, wenn weitere Seiten hinzukommen — genau das wäre bei \texttt{Download} und \texttt{Share} passiert. Die zentrale Registrierung stellt sicher, dass alle Routen derselben Richtlinie unterliegen.
\subsection{Gestapelte Pull Requests}
Die Pull Requests wurden verkettet: Der erste ging gegen den Hauptbranch, jeder darauf aufbauende gegen den Branch seines Vorgängers. So enthält jeder Pull Request genau die Änderungen seines Backlog Items, was die Reviewbarkeit erhöht. Der Preis zeigte sich beim Zusammenführen: Sobald ein Vorgänger übernommen war, musste der Nachfolger umgestellt werden, was in Azure DevOps die bereits abgegebenen Freigaben zurücksetzt. Bei der vorliegenden Zahl aufeinander aufbauender Items überwog der Gewinn an Reviewbarkeit diesen Aufwand deutlich.
+15 -2
View File
@@ -1,5 +1,18 @@
\section{Document Explorer} \section{Document Explorer}
\label{sec:document-explorer} \label{sec:document-explorer}
% TODO: PBI 9295 — /documents Seite, ListObjectsV2, Empty State, Fehlerbehandlung \subsection{Umfang und Darstellung}
% Abbildung~\ref{fig:documents-page-model}, \ref{fig:document-view-model}, \ref{fig:documents-table-body}
Der Document Explorer war das erste umgesetzte Backlog Item und mit acht Aufwandspunkten das umfangreichste. Er umfasst die Grundstruktur des Moduls: Seite, Dienst, Speicherclient, Modelle sowie Einbindung in Navigation und Autorisierung. Die ursprüngliche Kacheldarstellung wurde auf Zeilendarstellung geändert, da Auswahlboxen für den ZIP-Download, Download- und Teilen-Schaltflächen sowie eine Vorschau im Backlog standen, für die eine Kachel keinen natürlichen Platz geboten hätte; die Tabelle ist zudem um weitere Spalten erweiterbar.
\subsection{Ableitung des Dokumententyps}
Der Typ eines Dokuments ergibt sich aus dem Unterordnernamen und wird gegen eine Aufzählung abgeglichen. Im Review schlug \emph{Timo Walter} vor, die Zeichenketten-Umwandlung mit Nichtbeachtung der Groß- und Kleinschreibung zu verwenden statt einer eigenen Zuordnung; beim Hinzufügen eines Typs muss so nur die Aufzählung ergänzt werden. Schlägt die Umwandlung fehl, gilt das Dokument als untypisiert. Eine bewusste Ausnahme betrifft die Übersetzung: Fehlt für einen bekannten Typ der Eintrag in der Ressourcendatei, wird eine Ausnahme ausgelöst statt still auf den Ordnernamen zurückzufallen — eine fehlende Übersetzung ist eine unvollständige Implementierung.
\subsection{Leerer Zustand und Fehlerbehandlung}
Zwei Situationen ohne anzeigbare Dokumente sind zu unterscheiden. Der \textbf{leere Zustand} tritt ein, wenn der Kunde berechtigt ist, aber keine Dokumente vorliegen — insbesondere unmittelbar nach der automatischen Ordneranlage; er wird mit einem Hinweis dargestellt. Ein \textbf{Fehler} liegt vor, wenn die Dokumente nicht geladen werden konnten; im Review wurde angemerkt, dass dieser Fall nicht in einer stillschweigend leeren Seite münden darf.
\subsection{Die bekannte Schwäche}
Bereits im Review fragte \emph{Hanna Ebner}, warum für jeden Kunden eine eigene Abfrage nötig sei. Die Antwort war, dass die Schnittstelle dies nicht anders zulässt, verbunden mit dem Verweis auf das eigens angelegte Recherche-Item. Die Schwäche wurde bewusst zurückgestellt; die tatsächliche Lösung beschreibt Abschnitt~\ref{sec:folder-management}.
+20 -10
View File
@@ -1,13 +1,23 @@
\section{Dokumenten-Downloads} \section{Downloads}
\label{sec:downloads} \label{sec:downloads}
% TODO: PBI 9299 (Einzeldownload), PBI 9296 (ZIP-Download) \subsection{Einzeldownload über zeitlich begrenzte Zugriffs-URLs}
% Streaming: S3 GetObject → ZipArchive → HTTP Response Stream
% Abbildung~\ref{fig:zip-download-action}
% \begin{figure}[H] Für den Einzeldownload wird eine zeitlich begrenzte Zugriffs-URL erzeugt, mit der der Browser das Dokument unmittelbar vom Speicher abruft \autocite{aws-presigned-urls}. Die Datenübertragung läuft nicht über die Anwendung, und die URL ist nur wenige Minuten gültig. Die Berechtigungsprüfung findet \emph{vor} dem Erzeugen der URL statt, da die URL selbst keine Prüfung trägt: Sie darf nur für einen Schlüssel erzeugt werden, der nachweislich im Kundenordner liegt. Genau hier setzte der überwiegende Teil des Reviews an und führte zur in Abschnitt~\ref{sec:architecture} beschriebenen Einführung eigener Pfadtypen. Zusätzlich musste sichergestellt werden, dass das Dokument unter seinem ursprünglichen Namen ankommt.
% \centering
% \includegraphics[width=0.9\textwidth]{figures/diagrams/zip-stream.pdf} \subsection{ZIP-Download}
% \caption{ZIP-Download-Ablauf}
% \label{fig:zip-stream} Jede Zeile erhält eine Auswahlbox für den ZIP-Download; die Schaltfläche ist deaktiviert, solange nichts ausgewählt ist. Abbildung~\ref{fig:zip-stream} zeigt den Ablauf.
% \end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=0.95\textwidth]{figures/diagrams/zip-stream.pdf}
\caption{Ablauf des ZIP-Downloads}
\label{fig:zip-stream}
\end{figure}
Das Archiv wird erst beim Klick erzeugt und entsteht nie vollständig im Arbeitsspeicher: Die Anwendung öffnet die Antwort als Archivstrom, liest die Dokumente nacheinander aus dem Speicher und schreibt sie unmittelbar als Einträge hinein \autocite{dotnet-zip}. Da die Gesamtgröße unbegrenzt ist, würde ein vollständiger Aufbau den Speicherbedarf von der größten Auswahl abhängig machen; beim Strömen bleibt er konstant.
\subsection{ZIP auch bei einer einzelnen Datei}
Im Review fragte \emph{Sarah Hinzmann}, ob bei nur einer ausgewählten Datei ein Archiv erzeugt werden solle. Die Entscheidung fiel für das Archiv: Die Schaltfläche „Als ZIP herunterladen" soll vorhersagbar ein Archiv liefern; wer eine einzelne Datei unverpackt benötigt, verwendet die Download-Schaltfläche in der Zeile.
+11 -3
View File
@@ -1,6 +1,14 @@
\section{Typfilter und Paginierung} \section{Typfilter und Paginierung}
\label{sec:filter-pagination} \label{sec:filter-pagination}
% TODO: PBI 9749 (Typfilter), PBI 9731 (Pagination) \subsection{Filterung nach Dokumententyp}
% ContinuationToken statt Offset-Pagination
% Abbildung~\ref{fig:pagination-continuation-token} Unterhalb der Suchleiste steht je Dokumententyp ein Bedienelement zum Ein- und Ausblenden. Die Auswahl wird als Abfrageparameter übertragen und im Dienst ausgewertet. Da die Werte aus der Adresszeile stammen, muss die Anwendung mit manipulierten oder veralteten Werten rechnen; \emph{Robin Noack} schlug vor, nicht erkennbare Werte stillschweigend zu verwerfen — ein unbekannter Filterwert ist in der Regel ein veralteter Link, und die Mandantentrennung geschieht über das Präfix, nicht über den Filter. Ein zweiter Reviewhinweis führte zu einer geschärften Benennung, da sich der Typ für eine Menge von Dokumententypen vom Einzeltyp nur durch ein Zeichen unterschied.
\subsection{Paginierung mit Fortsetzungsmerkmalen}
Die Paginierung folgt der Arbeitsweise des Speichers, der Ergebnisse blockweise mit Fortsetzungsmerkmalen liefert \autocite{aws-listobjectsv2}. Ein direkter Sprung auf eine beliebige Seite ist nicht möglich, ohne alle vorhergehenden Blöcke abzurufen; die Akzeptanzkriterien verlangten dies auch nicht — gefordert waren Vor- und Zurück-Navigation. Ein aus den bestehenden Houston-Seiten übernommener Wert für die maximale Seitengröße (NFA-4) stammte, wie \emph{Robin Noack} anmerkte, dort aus einer Beschränkung des ITSM-Systems und wurde angepasst. Diskutiert wurde auch, ob die vom Benutzer wählbare Seitengröße nach oben begrenzt werden sollte.
\subsection{Zusammenspiel mit Freigabelinks}
Ein Freigabelink auf ein Dokument ist wertlos, wenn er stets auf der ersten Seite landet. Die Weiterleitung setzt deshalb nicht nur die Sprungmarke, sondern auch die Abfrageparameter für die richtige Seite.
+20 -6
View File
@@ -1,8 +1,22 @@
\section{Ordnerstruktur und Kundenordner-Lookup} \section{Ordnerverwaltung und Kundenordner-Lookup}
\label{sec:folder-management} \label{sec:folder-management}
% TODO: PBI 9560 (Ordneranlage beim ersten Seitenaufruf), PBI 10018 (O(1)-Lookup) \subsection{Gemeinsame Umsetzung zweier Backlog Items}
% OrgSlug: deterministische, S3-konforme Normalisierung des Efecte-Namens (Umlaute bleiben erhalten)
% IBM-Naming-Constraints: \cite{ibm-s3-naming} Die automatische Ordneranlage und der neue Kundenordner-Lookup wurden in einem gemeinsamen, begründet zusammengelegten Pull Request umgesetzt, da der Lookup in seiner Rückfallebene den Fall benötigt, dass kein Ordner existiert und die Struktur angelegt werden muss.
% ResolveCustomerPrefixAsync, Self-Healing-Rename
% Abbildung~\ref{fig:org-slug}, \ref{fig:resolve-customer-prefix}, \ref{fig:rename-customer-folder} \subsection{Normalisierung des Firmennamens}
Der erwartete Ordnername entsteht aus dem Firmennamen des Benutzers. Da dieser aus einem Fremdsystem stammt und beliebige Zeichen enthalten kann, wird er normalisiert: umschließende Leerzeichen entfernt, strukturell bedeutsame oder nicht darstellbare Zeichen ersetzt, führende und abschließende Punkte verhindert, die Länge begrenzt. Die Normalisierung ist \textbf{deterministisch} — Voraussetzung für den Direktzugriff — und erhält \textbf{Umlaute}, weil der Ordner in Filestash von Menschen gelesen wird. Im Review fragte \emph{Robin Noack}, ob die Einschränkungen ausreichen; die Zulässigkeit wurde anhand der Herstellerdokumentation bestätigt \autocite{ibm-s3-naming, aws-s3-naming}.
\subsection{Umsetzung des Lookups}
Die in Abschnitt~\ref{sec:lookup-decision} beschriebene dreistufige Auflösung wurde in einer Methode zusammengefasst — der einzigen Stelle, an der ein Kundenpräfix entsteht. Der erste Schritt fragt die Metadaten des erwarteten Markers ab, der zweite wiederholt dies mit dem um die Organisations-ID ergänzten Namen (Kollisionsfall), erst der dritte greift auf das lineare Verfahren zurück. Beim Prüfen, ob ein Zielname frei ist, wird zusätzlich geprüft, ob bereits Objekte unterhalb des Zielpräfixes liegen; ein Zielname gilt nur als frei, wenn weder ein fremder Marker noch Inhalte vorhanden sind.
\subsection{Umbenennen als Kopiervorgang}
Der Speicher kennt keine Umbenennung; ein Objekt lässt sich nur kopieren und anschließend löschen. Eine Umbenennung ist daher nicht unteilbar — sie kann abbrechen, und Objekte liegen teils am alten, teils am neuen Ort. Umgesetzt wurde dies über zwei Maßnahmen: Der \textbf{Marker wird zuletzt kopiert}, sodass das Ziel von einer parallelen Anfrage nicht als Kundenordner erkannt wird, solange er fehlt; und die Fehlerbehandlung entfernt bereits erzeugte Teilkopien, damit der Vorgang wiederholbar ist. Die zweite Maßnahme gilt nur, solange kein zweiter Vorgang dieselbe Umbenennung ausführt — Gegenstand des folgenden Abschnitts.
\subsection{Auslieferung}
Der zugehörige Pull Request wurde am 26.~August zusammengeführt, nachdem \emph{Robin Noack} zwei Anmerkungen eingebracht hatte: zur Ausreichung der Namensbeschränkungen und zu der Frage, ob ein Ordner vor seiner Erzeugung auf Fremdbesitz geprüft wird. Letztere ließ sich über die Zustände des Ordnermodells beantworten — erzeugt wird nur für einen Schlüssel im Zustand \emph{fehlend}. Mit der Produktivsetzung am 3.~September ist der Lookup mit konstanter Aufrufzahl wirksam; das lineare Verfahren dient nur noch als Rückfallebene.
+18 -4
View File
@@ -1,6 +1,20 @@
\section{PDF-Modal-Vorschau und Share-Link-Previews} \section{PDF-Vorschau und Freigabelinks}
\label{sec:pdf-preview} \label{sec:pdf-preview}
% TODO: PBI 9300 (PDF-Modal), PBI 9294 (Share-Link-Previews) \subsection{PDF-Vorschau im Modal}
% Pre-Signed URLs, zeitlich begrenzte Gültigkeit
% Abbildung~\ref{fig:presigned-url} Die Vorschau öffnet PDF-Dokumente in einem überlagerten Fenster über dieselbe zeitlich begrenzte Zugriffs-URL, die auch dem Download zugrunde liegt (Abbildung~\ref{fig:shot-pdf-preview} im Anhang); für Nicht-PDF-Dateien wird keine Vorschau angeboten. Eine Prüfung der Dateigröße wurde im Approval-Termin bewusst verworfen, da jede Grenze willkürlich wäre und der Browser PDF-Dokumente ohnehin fortlaufend darstellt.
\subsection{Aufbau der Freigabelinks}
Ein Freigabelink verweist auf eine eigene Seite, deren Adresse den relativen Pfad des Dokuments kodiert enthält. Kodiert wird nur der Anteil unterhalb des Kundenordners, um Pfadtrennzeichen und Sonderzeichen unbeschadet zu transportieren. Dass der Kundenordner nicht Bestandteil des Links ist (Listing~\ref{lst:document-id}), bewirkt, dass der Link keinen Hinweis auf die Organisation enthält und ohne Anmeldung nicht auflösbar ist. Die Seite zeigt keinen Dokumentinhalt, sondern liefert Metaangaben für die Vorschau und leitet auf die Dokumentenliste weiter, wobei Sprungmarke und passende Seite angesteuert werden.
\subsection{Vorschau in Messengern}
Zweck der Freigabeseite ist, dass ein in einem Messenger geteilter Link mit Titel und Symbol dargestellt wird. Dazu werden Metaangaben ausgeliefert: Dokumentname als Titel, Typsymbol als Bild, Adresse der Dokumentenliste als Ziel. Abbildung~\ref{fig:shot-share-teams} im Anhang zeigt das Ergebnis in Microsoft Teams, Abbildung~\ref{fig:shot-share-target} das Ziel in Houston. Da die Typsymbole als Vektorgrafiken vorliegen, die Vorschaudienste gängiger Messenger nicht zuverlässig darstellen, mussten sie zusätzlich als Rastergrafiken bereitgestellt werden; im Pull Request wurde vermerkt, dass diese bei einer Gestaltungsänderung nachzuziehen sind.
\subsection{Sichtbarkeit der Vorschaubilder}
Damit ein Messenger eine Vorschau erzeugen kann, muss er das Bild ohne Anmeldung abrufen können. Der Freigabe-Endpunkt ist deshalb anonym erreichbar — die einzige Stelle des Moduls ohne Autorisierung. Entschärft wird das durch den Aufbau der Dokumentkennung: Sie ist die Base64-Kodierung des relativen Pfades und trägt bereits alles, was die Seite ausgeben muss — Dokumentname als Titel, Dokumententyp und daraus den Pfad des Vorschaubildes sowie die Zieladresse (Listing~\ref{lst:share-page}). Der anonyme Endpunkt löst folglich keinen Aufruf an den Objektspeicher aus; er kann weder die Existenz eines Dokuments bestätigen noch Inhalte preisgeben noch als Hebel für Last dienen.
Bei den sieben festen Typsymbolen ist die freie Abrufbarkeit unbedenklich, da sie keine kundenbezogene Information enthalten. Benutzerdefinierte Symbole aus URL-Dateien werden daher \emph{nicht} für die Vorschau ausgeliefert — andernfalls müsste Dateiinhalt über einen nicht authentifizierten Pfad zugänglich gemacht werden. Die Entscheidung wurde im Pull Request festgehalten.
+64 -12
View File
@@ -1,15 +1,67 @@
\section{Bekannte Grenze: Race Conditions im Lookup} \section{Erkannte Grenze: Nebenläufigkeit im Lookup}
\label{sec:race-conditions} \label{sec:race-conditions}
% TODO: PBI 10070 — Houston läuft in mehreren Instanzen, In-Memory-Lock reicht nicht \subsection{Ausgangslage}
% Szenario A: Datenverlust beim parallelen Rename (Rollback löscht Dokumente des Gewinners)
% Szenario B: Mandantenvermischung beim gleichzeitigen Anlegen gleicher Efecte-Namen
% Lösungsansätze: Rollback-Absicherung, Conditional Writes, verteiltes Lock, prozessuale Migration
% Timebox: 4–6 Stunden (Stephan Janßen)
% \begin{figure}[H] Der Lesepfad des Kundenordner-Lookups ist unkritisch. Der in Abschnitt~\ref{sec:folder-management} beschriebene dritte Schritt ist jedoch \emph{verändernd}: Er benennt Ordner um und legt sie an, im Rahmen einer gewöhnlichen Seitenanfrage. Da Houston in mehreren Instanzen betrieben wird, reicht eine Sperre innerhalb einer Instanz nicht aus. Bei der Durchsicht des Codes wurden zwei Fehlerszenarien gefunden, die ausdrücklich formulierte Akzeptanzkriterien verletzen.
% \centering
% \includegraphics[width=0.9\textwidth]{figures/diagrams/race-condition-a.pdf} \subsection{Zwei Szenarien}
% \caption{Race Condition Szenario A: Datenverlust beim parallelen Rename}
% \label{fig:race-condition-a} Abbildung~\ref{fig:race-condition-a} zeigt exemplarisch Szenario~A; Tabelle~\ref{tab:race-conditions} stellt beide Fälle einander gegenüber.
% \end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=\textwidth]{figures/diagrams/race-condition-a.pdf}
\caption{Datenverlust bei gleichzeitigem Umbenennen}
\label{fig:race-condition-a}
\end{figure}
\begin{table}[H]
\centering
\begin{tabularx}{\textwidth}{@{} l X X @{}}
\toprule
\textbf{Szenario} & \textbf{Ursache} & \textbf{Auswirkung} \\
\midrule
A: Datenverlust beim Umbenennen &
Zwei Anfragen halten den Zielnamen für frei, da beide prüfen, bevor eine schreibt. A kopiert und löscht die Quelle; B's Fehlerbehandlung entfernt daraufhin dieselben (bereits von A geschriebenen) Objekte. &
Quelle und Kopie sind gelöscht — Datenverlust. \\
\addlinespace
B: Vermischung zweier Mandanten &
Der Marker wird ohne Bedingung geschrieben. Haben zwei Organisationen denselben Firmennamen, gewinnt beim gleichzeitigen Erstzugriff der zuletzt geschriebene Marker. &
Beide arbeiten im selben Ordner; zwischenzeitlich Abgelegtes bleibt im fremden Ordner — Vertraulichkeitsproblem, schwerwiegender als A. \\
\bottomrule
\end{tabularx}
\caption{Erkannte Nebenläufigkeitsszenarien im Kundenordner-Lookup}
\label{tab:race-conditions}
\end{table}
Nicht betroffen sind zwei gleichzeitige Anfragen derselben Organisation (identische Schlüssel) sowie Lesezugriffe während einer Verschiebung, da der Marker zuletzt kopiert wird. Das Problem beschränkt sich auf zwei \emph{unterschiedlich weit fortgeschrittene} verändernde Vorgänge.
\subsection{Erwogene Gegenmaßnahmen}
Tabelle~\ref{tab:race-countermeasures} stellt die vier erwogenen Ansätze gegenüber; der erste sollte unabhängig von der Serialisierungslösung umgesetzt werden, der vierte ist eine organisatorische statt technische Lösung.
\begin{table}[H]
\centering
\begin{tabularx}{\textwidth}{@{} l X @{}}
\toprule
\textbf{Ansatz} & \textbf{Wirkung} \\
\midrule
Absicherung der Fehlerbehandlung & Prüfung, ob der Marker der Quelle vor dem Entfernen der Teilkopien noch existiert; beseitigt A weitgehend, es bleiben überzählige Objekte statt Datenverlust. \\
\addlinespace
Bedingtes Schreiben & Marker nur schreiben, falls noch nicht vorhanden, macht das Beanspruchen eines Namens unteilbar und schließt B aus \autocite{aws-conditional-writes}; setzt Unterstützung durch den Speicher voraus. \\
\addlinespace
Instanzübergreifende Sperre & Sperre je Organisation über die Datenbank serialisiert den verändernden Teil sauber, kostet aber einen zusätzlichen Zugriff je Anfrage und ein Konzept für nicht freigegebene Sperren. \\
\addlinespace
Verlagerung aus dem Anfragepfad & Einmalige kontrollierte Migration der Bestandsordner; die Anwendung protokolliert nur noch Abweichungen vom Sollzustand — die Ursache verschwindet vollständig. \\
\bottomrule
\end{tabularx}
\caption{Erwogene Gegenmaßnahmen gegen die Nebenläufigkeitsszenarien}
\label{tab:race-countermeasures}
\end{table}
\subsection{Umgang mit dem Befund}
Der Befund wurde als eigenes Backlog Item mit erhöhter Priorität erfasst, vom laufenden Pull Request abgegrenzt und mit einem Zeitrahmen von vier bis sechs Stunden versehen.
Den Pull Request offenzuhalten, hätte den Lookup blockiert: Ob bedingtes Schreiben verfügbar ist, ließ sich ohne Rückfrage beim Betreiber nicht beantworten, deren Bearbeitungsdauer zuvor mit mehreren Wochen bemessen worden war. Der Lookup verbessert den Zustand bereits deutlich, und der verändernde Pfad wird nur beim ersten Zugriff einer Organisation durchlaufen — das Risikofenster besteht pro Kunde genau einmal. Die Akzeptanzkriterien des Folgeitems halten fest, dass eine reine Sperre innerhalb einer Instanz nicht akzeptiert wird und beide Szenarien durch Unit-Tests nachzubilden sind.
+17 -2
View File
@@ -1,5 +1,20 @@
\section{Anbindung des S3-Speichers} \section{Anbindung des S3-Speichers}
\label{sec:s3-client} \label{sec:s3-client}
% TODO: AWS SDK für .NET, IAmazonS3, keyed services in Program.cs, S3Settings (DEV/TEST/PROD Buckets) \subsection{Zugriff über das AWS SDK}
% Abbildung~\ref{fig:s3-settings-registration}, \ref{fig:s3-documents-client}
Der Zugriff auf den Speicher erfolgt über das AWS SDK für .NET. Obwohl der Speicher nicht bei Amazon betrieben wird, ist dies der naheliegende Weg: StorageGRID implementiert die S3-Schnittstelle, und das SDK lässt sich über eine abweichende Dienstadresse auf einen beliebigen kompatiblen Endpunkt richten; eine eigene Implementierung der Signaturberechnung wäre aufwendig und fehleranfällig gewesen.
Der \texttt{S3DocumentsClient} kapselt die verwendeten Operationen: Auflisten, Metadatenabruf, Erzeugen zeitlich begrenzter Zugriffs-URLs, Lesen von Objektinhalten sowie Anlegen, Kopieren und Löschen von Objekten. Diese Kapselung hält die SDK-spezifischen Typen aus der fachlichen Schicht und ermöglicht in Tests ein Mock (siehe Abschnitt~\ref{sec:unit-tests}).
\subsection{Konfiguration mehrerer Speicher}
Houston verwendete bereits vor diesem Projekt einen S3-Speicher für das Dokumentationssystem; mit dem Dokumentenbereich kam ein zweiter, davon unabhängiger hinzu. In der ersten Fassung wurden die Einstellungen als eigenständiger Konfigurationssatz geführt. Im Review wurde angeregt, eine gemeinsame Struktur für S3-Einstellungen zu verwenden und die Verwendungen über benannte Registrierungen im Dienstcontainer auseinanderzuhalten \autocite{dotnet-keyed-di}. Ein dritter Speicher erfordert so lediglich einen weiteren Konfigurationsabschnitt und eine Registrierung.
\subsection{Umgebungen und Zugangsdaten}
Für die drei Umgebungen existiert je ein eigener Bucket mit getrennten Zugangsdaten, sodass ein fehlerhaft konfigurierter Entwicklungsstand nicht auf Produktivdaten zugreifen kann. Die Zugangsdaten liegen nicht im Quelltext, sondern werden über die Konfigurationsmechanismen der Anwendung bereitgestellt und im unternehmensweiten Passwortmanager hinterlegt.
\subsection{Auflisten von Objekten}
Die zentrale Leseoperation listet Objekte unterhalb des Kundenpräfixes über alle Typordner hinweg auf. Zwei Eigenschaften prägten die Umsetzung: Erstens liefert sie Ergebnisse blockweise mit einem Fortsetzungsmerkmal \autocite{aws-listobjectsv2}, das für die Paginierung genutzt wurde (siehe Abschnitt~\ref{sec:filter-pagination}); zweitens liefert sie keine benutzerdefinierten Metadaten — die Ursache des in Abschnitt~\ref{sec:lookup-research} beschriebenen Lookup-Problems. Platzhalterobjekte der Typordner und der Marker des Kundenordners werden herausgefiltert; beide erkennt der Dienst daran, dass ihr Schlüssel auf das Trennzeichen endet.
+14 -4
View File
@@ -1,6 +1,16 @@
\section{Serverseitige Suche} \section{Suche}
\label{sec:search} \label{sec:search}
% TODO: PBI 9298 — Suchleiste, titelbasiert, Teiltreffer, serverseitiges Filtern \subsection{Umsetzung}
% S3-API bietet keine direkte Filterung → Houston-seitiger Filter
% Abbildung~\ref{fig:document-search} Die Suche filtert die Dokumentenliste anhand des Namens und berücksichtigt Teiltreffer. Ein geleertes Suchfeld stellt die vollständige Liste wieder her. Im Approval-Termin wurde gefragt, ob auch eine Beschreibung durchsucht werden solle; da Dokumente keine tragen, wurde die Suche auf den Namen begrenzt.
\subsection{Der Begriff „serverseitig"}
Die Anforderung verlangt eine serverseitige Suche. Gemeint ist, dass die Filterung in der Anwendung stattfindet, nicht im Browser — eine Filterung im Browser würde voraussetzen, dass sämtliche Dokumente zuvor übertragen wurden, was mit der Paginierung unvereinbar wäre. Nicht umgesetzt ist eine Filterung durch den \emph{Speicher}: Houston listet die Objekte auf und filtert die Namen anschließend selbst. Im Review stellte \emph{Hanna Ebner} die Frage, ob sich das nicht direkt über die Schnittstelle lösen lasse, und musste verneint werden.
Im weiteren Verlauf brachte \emph{Timo Walter} die Abfragefunktion des Speichers ins Spiel. Die Prüfung ergab, dass diese Objektinhalte filtert, nicht Objektnamen — sie wählt aus strukturierten Dateien einzelne Datensätze aus \autocite{aws-s3-select}. Als zweite Möglichkeit wurde der Suchdienst des Speicherherstellers erwogen \autocite{storagegrid-search-integration}; dessen Verfügbarkeit stand noch nicht fest, die Anfrage lief bereits (siehe Abschnitt~\ref{sec:infrastructure}) und fiel später negativ aus. Auf Anregung des Reviewers wurde dieser Umstand in das Recherche-Item aufgenommen.
\subsection{Bewertung}
Die praktische Auswirkung ist derzeit gering, da die Suche nur mit der Dokumentenzahl eines einzelnen Kunden skaliert, nicht mit der Kundenzahl. Der Lookup wurde daher im Projektzeitraum gelöst, die Suchoptimierung blieb für den Ausblick.
+12 -3
View File
@@ -1,5 +1,14 @@
\section{Dokumenttypen und Icons} \section{Typ-Icons}
\label{sec:type-icons} \label{sec:type-icons}
% TODO: PBI 9549 — Typ-Mapping aus Unterordner, .resx-Lokalisierung \subsection{Eigene Symbole statt Symbolschrift}
% Abbildung~\ref{fig:document-type-mapping}, \ref{fig:document-types-resx}
Houston verwendet für Symbole eine gängige Symbolbibliothek, die für die sieben Dokumententypen keine passenden Motive enthielt — Begriffe wie „SLA-Report Ticketbearbeitung" lassen sich mit allgemeinen Symbolen nicht unterscheiden. Es wurden daher eigene Vektorgrafiken erstellt. Die Entwürfe stammen von mir und wurden in Abstimmung mit \emph{Hanna Ebner} ausgearbeitet, damit sie sich in Strichstärke, Abmessung und Bildsprache in die vorhandene Symbolbibliothek einfügen; alle greifen dieselbe Dokumentkontur auf und unterscheiden sich nur im Innenmotiv, sodass die Typzugehörigkeit auf einen Blick erkennbar ist (Abbildung~\ref{fig:type-icons} im Anhang).
\subsection{Einfärbung über Masken}
Eine Einbindung als gewöhnliche Grafik hätte eine feste Farbe bedeutet, die im dunklen Erscheinungsbild von Houston kaum sichtbar gewesen wäre. Gelöst wurde dies, indem die Vektorgrafiken als Maske über einer Hintergrundfläche dienen, die die aktuelle Textfarbe übernimmt; die eigenen Symbole lassen sich so über dieselben Gestaltungsklassen ansprechen wie die vorhandenen. Alle Grafiken müssen dieselben Abmessungen haben; im Review fiel auf, dass eine abwich, und die Maße wurden angeglichen.
\subsection{Standardsymbol}
Dokumente ohne erkennbaren Typ erhalten ein neutrales Standardsymbol (letzter Eintrag in Abbildung~\ref{fig:type-icons}). Damit trägt jede Zeile ein Symbol, und die fehlende Typzuordnung ist erkennbar, ohne wie ein Fehler zu wirken.
+18 -3
View File
@@ -1,5 +1,20 @@
\section{Support für URL-Dateien} \section{Unterstützung von URL-Dateien}
\label{sec:url-files} \label{sec:url-files}
% TODO: PBI 9301 — .url-Dateien parsen, Verlinkung beliebiger URLs (Teams, SharePoint) \subsection{Anwendungsfall}
% Abbildung~\ref{fig:url-file-parsing}
Nicht alle Unterlagen lassen sich sinnvoll in den Speicher kopieren; ein fortlaufend gepflegtes Dokument oder ein Kanal in einem Kollaborationswerkzeug soll verlinkt werden, da eine Kopie sofort veraltet wäre. Dafür werden \texttt{.url}-Dateien unterstützt: kleine Textdateien im INI-Format mit Zieladresse und optionalem Symbol, die in der Liste wie gewöhnliche Dokumente erscheinen, beim Anklicken jedoch auf die hinterlegte Adresse führen. Der Anwendungsfall wurde in der Feature-Analyse geklärt: Gemeint ist das Einbetten \emph{externer} Ressourcen, nicht das Teilen von Houston-Dokumenten nach außen (siehe Abschnitt~\ref{sec:feature-analysis}).
\subsection{Auswertung und Anzeigename}
Für das INI-Format wurde bewusst keine externe Bibliothek eingebunden: Benötigt wird ein einziger Wert aus einem Abschnitt, der in wenigen Zeilen selbst gelesen ist, und eine Bibliothek würde bei fehlerhaften Dateien vermutlich einen Fehler melden, während hier ein stiller Rückfall erforderlich ist. Kann keine gültige Adresse erkannt werden, wird die Datei wie eine gewöhnliche Datei zum Herunterladen angeboten. Die Endung \texttt{.url} wird für die Anzeige entfernt; \emph{Robin Noack} wies im Review darauf hin, dass die dafür verwendete Zeichenzahl als unmittelbare Zahl im Code stand und stattdessen die Länge der Endung selbst verwendet werden sollte, da beide sonst bei einer Änderung auseinanderlaufen.
\subsection{Symbole für Verknüpfungen}
Das Dateiformat sieht im Abschnitt \texttt{[InternetShortcut]} einen Eintrag \texttt{IconFile} vor, der unter Windows auf eine Symboldatei zeigt \autocite{nsis-shortcuts}. Ein Dateipfad ist im Browser nicht verwertbar, und aus den in Abschnitt~\ref{sec:pdf-preview} genannten Gründen werden benutzerdefinierte Symbole ohnehin nicht ausgeliefert. Der Eintrag wird daher umgedeutet: Er trägt den Namen eines Symbols, das die Anwendung bereits kennt. Zulässig sind die Klassen der eigenen Typsymbole, etwa \texttt{doc-type-icon-vertragsunterlagen}, sowie Klassen der in Houston vorhandenen Symbolbibliothek Boxicons, etwa \texttt{bx bxl-github} \autocite{boxicons}. Für die drei häufigsten Verknüpfungsziele — Microsoft Teams, SharePoint und OneDrive — wurden im selben Stil eigene Grafiken ergänzt (Abbildung~\ref{fig:type-icons} im Anhang).
Fehlt der Eintrag oder ist er leer, greift dieselbe Regel wie für gewöhnliche Dateien: Liegt die Verknüpfung in einem bekannten Typordner, erscheint dessen Symbol, sonst das Standardsymbol; ein unbekannter Wert führt damit nie zu einer leeren Zelle (Abbildung~\ref{fig:shot-url-icons} im Anhang). Da die Kundenbetreuer diese Dateien von Hand anlegen, wurden Format, zulässige Werte und Rückfallverhalten im internen Wiki dokumentiert und im Pull Request darauf verwiesen.
\subsection{Ausschluss vom ZIP-Download}
Verknüpfungsdateien erhalten keine Auswahlbox und können nicht Teil eines Archivs werden, das sonst eine Datei mit einer möglicherweise nur intern auflösbaren Verknüpfung enthielte. Da eine Verknüpfung fachlich kein Dokument ist, wäre ihre Aufnahme irreführend.
+2 -14
View File
@@ -1,18 +1,6 @@
\section{Ausgangssituation} \section{Ausgangssituation}
\label{sec:initial-situation} \label{sec:initial-situation}
\section{Ausgangssituation} Die Idee besteht seit November 2023: \emph{Nicole Kimmel} legte damals das Feature~484 „Dokumente" mit zwei Stichpunkten an, ohne einen Lösungsansatz zu beschreiben.
\label{sec:initial-situation}
Die Idee, Kunden über Houston Zugang zu ihren Dokumenten zu ermöglichen, besteht seit November 2023: \emph{Nicole Kimmel} legte damals das Feature~484 „Dokumente" mit zwei Stichpunkten an — „Vertrag, Betriebshandbuch, Feinkonzepte an zentraler Stelle abgelegt" und „Rechnungen einsehbar". Diese Notiz blieb über zweieinhalb Jahre nahezu unverändert im Backlog und spiegelt die damaligen Bedürfnisse wider, ohne einen konkreten Lösungsansatz zu beschreiben. Zum Projektbeginn im Sommer 2026 wurden Verträge, Berichte und ähnliche Unterlagen punktuell per E-Mail oder Dateiablage bereitgestellt. Sie lagen ohne einheitlichen Zugangspunkt verteilt, mussten von Kunden aktiv angefordert werden und boten weder einen Überblick nach Dokumententyp noch einen technisch mandantengetrennten Zugriff. Zudem war kein S3-Speicher provisioniert; die Infrastruktur — drei Buckets (DEV, TEST, PROD) bei Advanced Unibyte auf Basis von NetApp StorageGRID — musste erst im Projektverlauf beantragt werden.
Zum Zeitpunkt des Projektbeginns im Sommer 2026 gab es keinen strukturierten Prozess, über den Kunden selbstständig auf ihre Dokumente zugreifen konnten. Verträge, Berichte und ähnliche Unterlagen wurden punktuell per E‑Mail oder über Dateiablagen bereitgestellt. Daraus ergaben sich mehrere Probleme:
\begin{itemize}
\item \textbf{Fehlende Zentralisierung:} Dokumente lagen verteilt in E‑Mails, Dateiablagen und lokalen Verzeichnissen ohne einheitlichen Zugangspunkt.
\item \textbf{Kein Self-Service für Kunden:} Kunden mussten Dokumente aktiv anfordern, anstatt sie eigenständig abrufen zu können.
\item \textbf{Kein strukturierter Überblick:} Eine Übersicht nach Dokumententypen oder zeitlichem Verlauf war nicht vorhanden.
\item \textbf{Kein sicherer, mandantengetrennter Zugriff:} Es existierte kein technischer Mechanismus, der sicherstellte, dass ein Kunde ausschließlich seine eigenen Dokumente einsehen konnte.
\end{itemize}
Auf technischer Seite war kein geeigneter S3-Speicher provisioniert und die Houston-Anwendung kannte keine Verbindung zu einem externen Objektspeicher. Die Bereitstellung der notwendigen Infrastruktur — drei S3-Buckets (DEV, TEST, PROD) bei Advanced Unibyte auf Basis von NetApp StorageGRID — musste erst im Laufe des Projekts beantragt und eingerichtet werden.
+4 -15
View File
@@ -1,22 +1,11 @@
\section{Beteiligte und Rollen}
\label{sec:participants}
\section{Projektbeteiligte} \section{Projektbeteiligte}
\label{sec:participants} \label{sec:participants}
Das Projekt wurde durch eine strukturierte Zusammenarbeit verschiedener Beteiligter mit klar definierten Rollen umgesetzt. Als \textbf{Praktikant und Entwickler} (\emph{Linus Nagel}) war ich für Anforderungsklärung, Konzeption, Implementierung aller Product Backlog Items, Akzeptanzkriterien und Dokumentation verantwortlich.
In meiner Rolle als \textbf{Praktikant und Entwickler} (\emph{Linus Nagel}) war ich für die gesamte technische Umsetzung verantwortlich: Anforderungsklärung, Konzeption, Implementierung aller Product Backlog Items, das Verfassen der Akzeptanzkriterien und die Erstellung dieser Dokumentation. \emph{Sarah Hinzmann} übernahm die \textbf{betriebliche Betreuung}, koordinierte Abstimmungen und war an Code-Reviews der Abschlussphase beteiligt. \emph{Thomas Drewermann} war \textbf{Product Owner}: Er arbeitete Feature~484 im Juni 2026 aus, definierte den Umfang und beantwortete Rückfragen.
Die \textbf{betriebliche Betreuung} übernahm \emph{Sarah Hinzmann}. Sie koordinierte die Abstimmungen, begleitete das Projekt von der Themenvergabe bis zur Abgabe und war an Code-Reviews der Abschlussphase beteiligt. Die \textbf{technische Qualitätssicherung} lag beim Team der Unicorn Development: \emph{Timo Walter} (Code-Reviews der frühen PRs, Architektur-Rückfragen), \emph{Robin Noack} (Reviews der Schlussphase) und \emph{Hanna Ebner} (Reviews, Oberflächenentscheidungen, Clickdummy der Typfilterung). \emph{Maria-Lena Andersz} führte die \textbf{Abnahmetests} durch. Weitere Beteiligte: \emph{Stephan Janßen} (Schätzung, Backlog-Pflege), \emph{Christiana Sobik} (Backlog-Pflege), \emph{Bianco Veigel} (Sprint-Planung) und \emph{Nicole Kimmel} (ursprüngliche Anforderung, 2023).
Als \textbf{Product Owner und fachlicher Ansprechpartner} fungierte \emph{Thomas Drewermann}. Er arbeitete das Feature~484 im Juni 2026 vollständig aus, definierte den Umfang, beantwortete Rückfragen während der Feature-Analyse und begleitete den Projektverlauf fachlich.
Die \textbf{technische Qualitätssicherung} übernahm das Entwicklerteam der Unicorn Development: \emph{Timo Walter} führte die Code-Reviews der frühen Pull Requests durch und stellte Rückfragen zu Architekturentscheidungen; \emph{Robin Noack} übernahm die Reviews in der Schlussphase. \emph{Hanna Ebner} erarbeitete das UI-Konzept und den Clickdummy. Das gesamte Team war an der Aufwandsschätzung der Product Backlog Items beteiligt.
Die \textbf{Abnahmetests} wurden durch \emph{Maria-Lena Andersz} durchgeführt.
Weitere Beteiligte in unterstützenden Rollen: \emph{Stephan Janßen} (Schätzung und Backlog-Pflege), \emph{Christiana Sobik} (Backlog-Pflege), \emph{Bianco Veigel} (Sprint-Planung), \emph{Nicole Kimmel} (ursprüngliche Anforderung, 2023).
\begin{table}[H] \begin{table}[H]
\centering \centering
@@ -29,7 +18,7 @@ Thomas Drewermann & Product Owner, fachliche Ausarbeitung Feature 484 \\
Sarah Hinzmann & Betriebliche Betreuung, PR-Reviews \\ Sarah Hinzmann & Betriebliche Betreuung, PR-Reviews \\
Timo Walter & Code-Review (PRs 2154–2164), technische Rückfragen \\ Timo Walter & Code-Review (PRs 2154–2164), technische Rückfragen \\
Robin Noack & Code-Review (PRs 2180–2196) \\ Robin Noack & Code-Review (PRs 2180–2196) \\
Hanna Ebner & UX/UI, Clickdummy \\ Hanna Ebner & Oberflächenentscheidungen, Clickdummy Typfilter \\
Maria-Lena Andersz & Abnahmetest \\ Maria-Lena Andersz & Abnahmetest \\
Stephan Janßen & Schätzung, Refinement \\ Stephan Janßen & Schätzung, Refinement \\
\bottomrule \bottomrule
+3 -8
View File
@@ -1,13 +1,8 @@
\section{Projektbeschreibung} \section{Projektbeschreibung}
\label{sec:project-description} \label{sec:project-description}
\section{Projektbeschreibung} Das Projekt \emph{Houston Dokumente} stellt Kunden im Kundenportal Houston einen zentralen Bereich bereit, in dem sie ihre Dokumente einsehen und herunterladen können. Die Dokumente liegen in einem S3-Speicher; Mitarbeiter pflegen sie über das interne Werkzeug Filestash, Kunden rufen sie über eine neue Houston-Seite strukturiert ab.
\label{sec:project-description}
Das Projekt \emph{Houston Dokumente} hat zum Ziel, Kunden im Kundenportal Houston einen zentralen Bereich bereitzustellen, in dem sie ihre Dokumente einsehen und herunterladen können. Die Dokumente werden in einem S3-Speichersystem abgelegt und gepflegt; den Kunden werden sie über eine neue Houston-Seite zugänglich gemacht. Der Dokumentenbereich unterscheidet sieben fachlich definierte Dokumententypen — darunter Service-Protokolle, SLA-Reports, Monitoring Reports und Vertragsunterlagen — und bietet Suche, Typfilter, Paginierung, PDF-Viewer, Einzel- und ZIP-Download sowie URL-Dateien (siehe Abschnitt~\ref{sec:target-situation}).
Bisher existierte kein einheitlicher, zentraler Zugangspunkt für kundenbezogene Dokumente wie Verträge, Berichte oder Protokolle. Diese wurden punktuell per E‑Mail oder Dateiablage bereitgestellt und waren für Kunden nicht selbstständig abrufbar. Die neue Dokumentenseite in Houston löst diese Situation ab: Mitarbeiter pflegen die Dokumente über ein internes Dateiverwaltungswerkzeug direkt im S3-Speicher, Kunden können sie anschließend strukturiert abrufen. Technisch umfasst das Projekt die S3-Anbindung über das AWS SDK für .NET, ein rollenbasiertes Berechtigungskonzept über Microsoft Entra~ID sowie die automatische Ordneranlage beim ersten Seitenaufruf. Schreibzugriffe durch Kunden sind nicht vorgesehen.
Der Dokumentenbereich unterscheidet sieben fachlich definierte Dokumententypen — darunter Service-Protokolle, SLA-Reports, Monitoring Reports und Vertragsunterlagen — und gliedert die Anzeige anhand dieser Typen. Darüber hinaus umfasst das Projekt eine Suchfunktion, Filter nach Dokumententyp, Paginierung, einen PDF-Viewer, den Download einzelner Dateien sowie das Herunterladen mehrerer Dokumente als ZIP-Archiv. Zusätzlich werden sogenannte URL-Dateien unterstützt, mit denen beliebige Webadressen — etwa Links zu Teams-Kanälen oder SharePoint-Seiten — in der Dokumentenliste verknüpft werden können.
Das Projekt umfasst die vollständige Integration des Dokumentenbereichs in die bestehende Houston-Webanwendung. Dazu zählen die Anbindung an den S3-Speicher über das AWS SDK für .NET, ein rollenbasiertes Berechtigungskonzept über Microsoft Entra~ID sowie die automatische Anlage der Ordnerstruktur für neue Kunden beim ersten Seitenaufruf. Nicht Bestandteil des Projekts sind Schreibzugriffe durch Kunden sowie die Anlage oder Bearbeitung von Dokumenten durch den Kunden selbst; dies bleibt Aufgabe der internen Mitarbeiter über das Dateiverwaltungswerkzeug.
+4 -7
View File
@@ -1,14 +1,11 @@
\section{Projektabgrenzung} \section{Projektabgrenzung}
\label{sec:project-scope} \label{sec:project-scope}
\section{Projektabgrenzung}
\label{sec:project-scope}
Folgende Punkte sind explizit \emph{nicht} Bestandteil des Projekts: Folgende Punkte sind explizit \emph{nicht} Bestandteil des Projekts:
\begin{itemize} \begin{itemize}
\item \textbf{Kein Schreibzugriff für Kunden:} Kunden können Dokumente ausschließlich einsehen und herunterladen. Das Hochladen, Bearbeiten oder Löschen von Dokumenten durch den Kunden ist nicht vorgesehen. \item \textbf{Kein Schreibzugriff für Kunden:} Kunden können Dokumente nur einsehen und herunterladen.
\item \textbf{Kein internes Upload-UI in Houston:} Das Hochladen und Verwalten von Dokumenten durch WorkSimple-Mitarbeiter erfolgt ausschließlich über Filestash, einen externen S3-Browser. Eine eigene Upload-Oberfläche in Houston wurde nicht entwickelt. \item \textbf{Kein internes Upload-UI in Houston:} Die Pflege durch Mitarbeiter erfolgt ausschließlich über Filestash.
\item \textbf{Keine freie Ordnerstruktur intern:} Interne Mitarbeiter können über Filestash keine beliebigen Ordner anlegen, die dem Kunden angezeigt werden. Nur die sieben definierten Typordner sind für Kunden sichtbar; weitere Ordner werden ignoriert. \item \textbf{Keine freie Ordnerstruktur intern:} Für Kunden sind nur die sieben Typordner sichtbar; weitere Ordner werden ignoriert.
\item \textbf{Keine Dashboard-Kacheln im PidI-Umfang:} Geplante Erweiterungen wie Dashboard-Kacheln für Tenant-Härtung aus Security-Assessment-Metadaten oder eine Anzeige zuletzt hinzugefügter Dokumente sind als Feature-Creep im Backlog erfasst, aber nicht Teil des PidI-Projekts. \item \textbf{Keine Dashboard-Kacheln im PidI-Umfang:} Erweiterungen wie Dashboard-Kacheln für Tenant-Härtung oder eine Anzeige zuletzt hinzugefügter Dokumente sind als Feature-Creep im Backlog erfasst, aber nicht Teil des PidI-Projekts.
\end{itemize} \end{itemize}
+6 -9
View File
@@ -1,10 +1,7 @@
\section{Systemlandschaft} \section{Systemlandschaft}
\label{sec:system-landscape} \label{sec:system-landscape}
\section{Systemlandschaft} Abbildung~\ref{fig:system-context} zeigt den Systemkontext des Dokumentenbereichs.
\label{sec:system-landscape}
Der Dokumentenbereich ist in die bestehende Systemlandschaft von WorkSimple eingebettet. Abbildung~\ref{fig:system-context} zeigt den Systemkontext und das Zusammenspiel der beteiligten Systeme.
\begin{figure}[H] \begin{figure}[H]
\centering \centering
@@ -13,12 +10,12 @@ Der Dokumentenbereich ist in die bestehende Systemlandschaft von WorkSimple eing
\label{fig:system-context} \label{fig:system-context}
\end{figure} \end{figure}
\textbf{Houston} ist das Kundenportal von WorkSimple. Es ist als ASP.NET-Core-Webanwendung mit Razor Pages implementiert und aggregiert Informationen aus mehreren internen Systemen zu einer einheitlichen Oberfläche für Kunden. Im Rahmen dieses Projekts wurde Houston um den Dokumentenbereich erweitert. \textbf{Houston} ist das Kundenportal von WorkSimple, eine ASP.NET-Core-Webanwendung mit Razor Pages, die um den Dokumentenbereich erweitert wurde.
\textbf{Efecte} ist das unternehmenseigene ITSM-Tool (IT Service Management). Es dient als zentrale Datenbasis für Kundeninformationen, darunter die eindeutige Organisations-ID jedes Kunden (\texttt{efecte-org-id}), die im Rahmen des Projekts als Autorisierungsmerkmal für den Zugriff auf den richtigen S3-Kundenordner verwendet wird. \textbf{Efecte} ist das ITSM-Tool des Unternehmens und liefert die eindeutige Organisations-ID jedes Kunden (\texttt{efecte-org-id}) als Autorisierungsmerkmal für den S3-Kundenordner.
\textbf{Microsoft Entra~ID} (ehemals Azure~AD) übernimmt die Authentifizierung und Autorisierung der Benutzer. Nach erfolgreicher Anmeldung erhält Houston ein Token, das unter anderem die Efecte-Organisations-ID und die zugewiesenen Anwendungsrollen des Benutzers enthält. Die neu eingeführte Rolle \texttt{Documents.Read} steuert den Zugriff auf den Dokumentenbereich. \textbf{Microsoft Entra~ID} (ehemals Azure~AD) übernimmt Authentifizierung und Autorisierung; das Token enthält die Efecte-Organisations-ID und die Anwendungsrollen, wobei \texttt{Documents.Read} den Zugriff steuert.
\textbf{S3-Speicher (NetApp StorageGRID)} ist die Ablage für alle Kundendokumente. Er wird von Advanced Unibyte betrieben und ist S3-kompatibel. Houston greift über das AWS SDK für .NET auf den Speicher zu. Jeder Kunde erhält einen eigenen Ordner im Bucket, der über ein S3-Objekt-Metadatum (\texttt{efecte-org-id}) identifiziert wird. \textbf{S3-Speicher (NetApp StorageGRID)} ist die von Advanced Unibyte betriebene, S3-kompatible Ablage für Kundendokumente, auf die Houston über das AWS SDK für .NET zugreift; jeder Kunde erhält einen eigenen, über \texttt{efecte-org-id} identifizierten Ordner.
\textbf{Filestash} ist ein webbasierter S3-Browser, der intern von WorkSimple-Mitarbeitern genutzt wird, um Dokumente in den S3-Speicher hochzuladen und zu verwalten. Filestash ist eine externe Anwendung und kein Bestandteil der Houston-Entwicklung. \textbf{Filestash} ist ein webbasierter S3-Browser für die interne Dokumentenpflege.
+2 -19
View File
@@ -1,23 +1,6 @@
\section{Zielsituation} \section{Zielsituation}
\label{sec:target-situation} \label{sec:target-situation}
\section{Zielsituation} Der Dokumentenbereich stellt auf \texttt{/documents} alle Dokumente des Kunden als Liste dar, gegliedert nach sieben Dokumententypen; leere Ordner erscheinen nicht. Er bietet Typfilter und serverseitige Titelsuche, Paginierung, Einzel- und ZIP-Download, eine PDF-Vorschau im modalen Viewer sowie zeitlich begrenzte Share-Link-Previews. \texttt{.url}-Dateien verlinken beliebige Webadressen — etwa Teams-Kanäle oder SharePoint-Seiten — in der Dokumentenliste.
\label{sec:target-situation}
Mit der Fertigstellung des Dokumentenbereichs erhalten Kunden im Kundenportal Houston erstmals einen strukturierten, selbstständig nutzbaren Zugang zu ihren Dokumenten. Die Zielsituation zeichnet sich durch eine zentrale Ablage im S3-Speicher und eine mandantengetrennte Darstellung in Houston aus. Die Mandantentrennung stellt sicher, dass jeder Kunde nur die Dokumente seines eigenen S3-Ordners sieht, identifiziert über die \texttt{efecte-org-id} aus dem Authentifizierungstoken; die Ordnerstruktur wird beim ersten Aufruf automatisch angelegt, sofern sie fehlt. Kunden haben ausschließlich lesenden Zugriff; die Pflege erfolgt durch Mitarbeiter über Filestash.
Der Dokumentenbereich bietet folgende zentrale Funktionen:
\begin{itemize}
\item \textbf{Document Explorer:} Auf der Seite \texttt{/documents} werden alle Dokumente des Kunden als Liste dargestellt, gegliedert nach sieben fachlich definierten Dokumententypen (Service-Protokoll, Abnahme-Dokumente, SLA-Reports Ticketbearbeitung, Monitoring Reports, Security Assessments, Abrechnungsdaten, Vertragsunterlagen). Leere Ordner werden nicht angezeigt.
\item \textbf{Typfilter und Suche:} Dokumente können nach Typ gefiltert und titelbasiert serverseitig durchsucht werden.
\item \textbf{Paginierung:} Große Dokumentenmengen werden seitenweise dargestellt.
\item \textbf{Downloads:} Einzelne Dokumente lassen sich direkt herunterladen; mehrere Dokumente können als ZIP-Archiv gebündelt heruntergeladen werden.
\item \textbf{PDF-Vorschau:} PDF-Dokumente können in einem modalen Viewer direkt im Browser angezeigt werden.
\item \textbf{Share-Link-Previews:} Für einzelne Dokumente können zeitlich begrenzte Freigabelinks erzeugt werden.
\item \textbf{URL-Dateien:} Spezielle \texttt{.url}-Dateien ermöglichen die Verlinkung beliebiger Webadressen — z.\,B. Teams-Kanäle oder SharePoint-Seiten — in der Dokumentenliste.
\item \textbf{Mandantentrennung:} Jeder Kunde sieht ausschließlich die Dokumente in seinem eigenen S3-Ordner, der über die \texttt{efecte-org-id} aus dem Authentifizierungstoken identifiziert wird.
\item \textbf{Automatische Ordneranlage:} Beim ersten Aufruf der Dokumentenseite wird die vollständige Ordnerstruktur für den Kunden im S3 angelegt, sofern sie noch nicht existiert.
\end{itemize}
Die Pflege der Dokumente erfolgt weiterhin durch WorkSimple-Mitarbeiter über Filestash, den internen S3-Browser. Kunden haben ausschließlich lesenden Zugriff.
+20 -11
View File
@@ -1,14 +1,23 @@
\section{Backlog-Schnitt und Schätzung} \section{Backlog und Entwicklungsprozess}
\label{sec:backlog} \label{sec:backlog}
% TODO: 14 PBIs + 1 Bug unter Feature 484 \subsection{Entwicklungsprozess}
% Prozess: DoR, DoD, Approval-Team, Sprints 15–17.2026
% Siehe Tabelle~\ref{tab:backlog} im Anhang
% Gantt: Die Unicorn Development arbeitet agil mit zweiwöchigen Sprints in Azure DevOps. Der verbindliche Softwareprozess — im internen XWiki dokumentiert und in Abbildung~\ref{fig:unicorn-process} wiedergegeben — beschreibt den Weg von der Kundenidee bis zur Abrechnung und ordnet jedem Schritt den Zustand des Work Items zu.
% \begin{figure}[H]
% \centering \begin{figure}[H]
% \includegraphics[width=\textwidth]{figures/diagrams/gantt-plan.pdf} \centering
% \caption{Zeitplanung über Sprints 15–17.2026} \includegraphics[width=\textwidth]{figures/worksimple/softwareprozess-unicorns.jpg}
% \label{fig:gantt} \caption{Softwareprozess der Unicorn Development mit den zugehörigen Work-Item-Zuständen}
% \end{figure} \label{fig:unicorn-process}
\end{figure}
Das Dokumentenfeature durchlief den Prozess ohne Abweichung; da es eine Erweiterung des eigenen Produkts Houston ist, entfielen nur die auftragsbezogenen Schritte. Die Freigabestufe erwies sich als wirksam — Rückfragen im Approval-Termin, insbesondere durch \emph{Timo Walter}, deckten mehrfach Lücken in den Akzeptanzkriterien auf.
\subsection{Schnitt der Product Backlog Items}
Alle Arbeiten hängen als Product Backlog Items am Feature~484. Tabelle~\ref{tab:backlog} im Anhang listet sie mit Kennung, Titel, Aufwand, Sprint und Zustand auf; Tabelle~\ref{tab:requirements} ordnet ihnen die Anforderungen aus Abschnitt~\ref{sec:requirements} zu.
Der Backlog-Schnitt erfolgte in zwei Phasen. Am 18.~Juni 2026 wurden acht Kern-Items angelegt (Document Explorer als Basis, die \emph{Feature-Creep}-Punkte Suche, Einzeldownload, ZIP-Download, PDF-Modal und URL-Dateien als eigene Items); am 7.~Juli kam die Typisierung über Icons hinzu, am 8.~Juli die automatische Ordneranlage. Während der Umsetzung folgten am 22.~Juli Paginierung und Typfilter aus der UI-Zuarbeit, am 28.~Juli die Recherche zur S3-Abfrage (Ergebnis am 12.~August im Kundenordner-Lookup-Item) und am 17.~August das Item zu Race Conditions (siehe Abschnitt~\ref{sec:race-conditions}).
Insgesamt sind es 15 Product Backlog Items und ein Bug; die Aufwände summieren sich auf 46 Story Points (35 in Sprint~15.2026, 11 in Sprint~16.2026). Alle vorgeschnittenen Items betreffen fachliche Funktionen, alle nachgeschobenen entstanden aus technischen Problemen — ein Muster, das in Abschnitt~\ref{sec:reflection} aufgegriffen wird.
+12 -4
View File
@@ -1,7 +1,15 @@
\section{Feature-Analyse} \section{Feature-Analyse}
\label{sec:feature-analysis} \label{sec:feature-analysis}
% TODO: 18.06.2026 — 4 Rückfragen von Linus an Thomas, Antworten im selben Ticket (Feature 484 Rev 17–32) Das Feature~484 „Dokumente" existierte seit November 2023 als zweizeilige Bedarfsnotiz. Am 10.~Juni 2026 arbeitete \emph{Thomas Drewermann} es zu einer vollständigen Feature-Beschreibung aus — einschließlich S3, Filestash als Verwaltungswerkzeug, den sieben Dokumententypen sowie den Abschnitten \emph{Feature-Creep} und \emph{Out of Scope}.
% - Autorisierung via Metadatum: ja
% - Filestash als externe Website (kein Custom-UI): ja Am 18.~Juni 2026 führte ich die Feature-Analyse durch; vier am selben Tag beantwortete Rückfragen entschieden Architekturfragen:
% - ...
\begin{enumerate}
\item \textbf{Autorisierung über Metadaten:} Die Efecte-Organisations-ID wird als Metadatum am Kundenordner gespeichert und löst die Berechtigung auf (siehe Abschnitt~\ref{sec:authorization}).
\item \textbf{Filestash als externes Werkzeug:} Filestash dient als Verwaltungsoberfläche, \emph{nicht} als Vorlage für einen Nachbau in Houston.
\item \textbf{Typisierung über Ordner statt Metadaten:} Der Dokumententyp wird über den Unterordner bestimmt.
\item \textbf{Bedeutung der URL-Dateien:} Gemeint ist das Einbetten \emph{externer} Ressourcen, nicht das Teilen von Houston-Dokumenten nach außen.
\end{enumerate}
Die dritte Entscheidung war die folgenreichste: Die Typabbildung über Ordner macht die Filterung günstig (Präfix-Listing), erzwingt aber die automatische Ordneranlage und erschwert die typübergreifende Suche.
+18 -12
View File
@@ -1,15 +1,21 @@
\section{Infrastrukturbeschaffung} \section{Beschaffung der S3-Infrastruktur}
\label{sec:infrastructure} \label{sec:infrastructure}
% TODO: Service Requests an Maschinenraum (2026-07-22), Provisionierung durch Advanced Unibyte, Der S3-Speicher stand zu Projektbeginn nicht bereit und musste über den Service-Desk beantragt werden. Da Advanced Unibyte ihn betreibt, war ein mehrstufiger Abstimmungsweg nötig; Abbildung~\ref{fig:timeline-infra} zeigt den Verlauf.
% 3 Buckets (DEV/TEST/PROD), Credentials in Passbolt (2026-07-27)
% Anfrage S3 Select (2026-07-30), Aktivierung durch AU (2026-08-07)
% Search Integration: von AU nicht bereitgestellt (2026-08-17)
% Timeline-Diagramm: \begin{figure}[H]
% \begin{figure}[H] \centering
% \centering \includegraphics[width=\textwidth]{figures/diagrams/timeline-infra.pdf}
% \includegraphics[width=\textwidth]{figures/diagrams/timeline-infra.pdf} \caption{Chronologie der Infrastrukturbeschaffung}
% \caption{Chronologie der Infrastrukturbeschaffung} \label{fig:timeline-infra}
% \label{fig:timeline-infra} \end{figure}
% \end{figure}
\subsection{Bereitstellung der Buckets}
Am 22.~Juli 2026 stellte ich den Service Request; er wurde am 23.~Juli \emph{Alexander Wagner} zugewiesen und am 27.~Juli abgeschlossen — drei Buckets (DEV, TEST, PROD) angelegt, Zugangsdaten in Passbolt hinterlegt. Da der Document Explorer ohnehin erst am 27.~Juli begann, entstand keine Verzögerung.
\subsection{Freischaltung zusätzlicher Funktionen}
Im Rahmen der Lookup-Recherche (siehe Abschnitt~\ref{sec:lookup-research}) kamen zwei S3-Funktionen in Betracht: \textbf{S3 Select} (\texttt{SelectObjectContent}), das Objektinhalte serverseitig per SQL-ähnlicher Abfrage filtert \autocite{aws-s3-select, storagegrid-s3-select}, und der \textbf{Search Integration Service} von StorageGRID, der Objektmetadaten in einen Elasticsearch-Index spiegelt \autocite{storagegrid-search-integration}.
Am 30.~Juli beantragte ich beide Funktionen; \emph{Lennart Meinert} kontaktierte am 4.~August Advanced Unibyte, am 7.~August wurde S3 Select für alle drei Umgebungen aktiviert. Den Search Integration Service bot Advanced Unibyte laut Mitteilung vom 17.~August nicht an; da bereits eine Lösung ohne serverseitige Suche umgesetzt war (siehe Abschnitt~\ref{sec:lookup-decision}), wurde der Request geschlossen. Zwischen Antrag und Klärung lagen vier Wochen — ein Ansatz, der auf einer erst zu beschaffenden Fremdleistung beruht, ist in einem solchen Projektzeitraum nicht belastbar.
+11 -2
View File
@@ -1,5 +1,14 @@
\section{Einarbeitung} \section{Einarbeitung}
\label{sec:onboarding} \label{sec:onboarding}
% TODO: S3-API, AWS SDK für .NET, StorageGRID-Dokumentation \subsection{S3 als Objektspeicher}
% Quellen: note-1785344962361 (S3-Recherche mit Timo), note-1785413874041
S3 (\emph{Simple Storage Service}) ist ein Objektspeicher, kein Dateisystem: Objekte werden über einen flachen Schlüsselraum adressiert, und was als Ordner erscheint, ist ein per \texttt{Delimiter} hierarchisch interpretiertes Präfix \autocite{aws-listobjectsv2}. Das prägt das Ablagekonzept (siehe Abschnitt~\ref{sec:s3-layout}). S3 bietet zudem \emph{keine} Suche über benutzerdefinierte Metadaten; ein Lookup erfordert das Auflisten aller Objekte mit clientseitiger Filterung — das zentrale technische Problem des Projekts (siehe Abschnitt~\ref{sec:lookup-research}).
\subsection{AWS SDK für .NET}
Der Zugriff erfolgt über \texttt{IAmazonS3} aus dem AWS SDK für .NET (\texttt{ListObjectsV2}, \texttt{GetObject}, \texttt{PutObject}, \texttt{CopyObject}, \texttt{DeleteObjects}). Da \texttt{IAmazonS3} eine Schnittstelle ist, lässt sie sich in Unit-Tests durch ein Mock ersetzen (siehe Abschnitt~\ref{sec:unit-tests}).
\subsection{NetApp StorageGRID}
Der Speicher wird von Advanced Unibyte auf Basis von NetApp StorageGRID betrieben. StorageGRID ist S3-kompatibel, implementiert die API aber nicht vollständig; dies betraf \texttt{SelectObjectContent} \autocite{storagegrid-s3-select} und den \emph{Search Integration Service} \autocite{storagegrid-search-integration} (siehe Abschnitt~\ref{sec:infrastructure}).
+31 -3
View File
@@ -1,5 +1,33 @@
\section{Anforderungserhebung} \section{Anforderungen}
\label{sec:requirements} \label{sec:requirements}
% TODO: Funktionale und nichtfunktionale Anforderungen aus den PBI-ACs Aus Feature-Beschreibung und Analyse ergaben sich die folgenden Anforderungen (Zuordnung zu Backlog Items in Tabelle~\ref{tab:requirements} im Anhang).
% Tabelle: Anforderungsmatrix (funktional / nichtfunktional → PBI)
\subsection{Funktionale Anforderungen}
\begin{itemize}
\item \textbf{FA-1 — Dokumentenliste:} Auf \texttt{/documents} werden alle Dokumente der Organisation aufgelistet; leere Typordner erscheinen nicht.
\item \textbf{FA-2 — Mandantentrennung:} Ein Benutzer sieht ausschließlich Dokumente seiner Organisation, zugeordnet über die \texttt{efecte-org-id}.
\item \textbf{FA-3 — Rollenbasierter Zugriff:} Der Navigationspunkt ist nur bei Berechtigung sichtbar; Aufruf ohne Berechtigung ergibt 403.
\item \textbf{FA-4 — Typisierung und Icons:} Jedes Dokument erhält ein aus dem Unterordner abgeleitetes Icon.
\item \textbf{FA-5 — Suche:} Serverseitige Titelsuche mit Teiltreffern.
\item \textbf{FA-6 — Typfilter:} Filterelemente je Dokumententyp zum Ein- und Ausblenden.
\item \textbf{FA-7 — Paginierung:} Seitenweise Darstellung mit benutzerdefinierter Seitengröße, analog zu den übrigen Houston-Seiten.
\item \textbf{FA-8 — Einzeldownload:} Download über eine zeitlich begrenzte Pre-Signed URL \autocite{aws-presigned-urls} unter Beibehaltung des Dateinamens.
\item \textbf{FA-9 — ZIP-Download:} Ausgewählte Dokumente werden als ZIP-Archiv mit S3-analoger Ordnerstruktur heruntergeladen.
\item \textbf{FA-10 — PDF-Vorschau:} PDF-Anzeige im Modal ohne vorherigen Download.
\item \textbf{FA-11 — Share-Links:} Freigabelink mit OpenGraph-Meta-Tags für Linkvorschau und Weiterleitung auf den Document Explorer.
\item \textbf{FA-12 — URL-Dateien:} \texttt{.url}-Dateien werden nach INI-Format ausgewertet und leiten auf die hinterlegte Adresse weiter; fehlt eine gültige URL, wird die Datei normal behandelt.
\item \textbf{FA-13 — Automatische Ordneranlage:} Beim Aufruf wird die Typordnerstruktur der Organisation angelegt, sofern sie fehlt.
\end{itemize}
\subsection{Nichtfunktionale Anforderungen}
\begin{itemize}
\item \textbf{NFA-1 — Skalierbarkeit des Lookups:} Die Auflösung des Kundenordners darf nicht linear mit der Kundenanzahl wachsen.
\item \textbf{NFA-2 — Fehlerbehandlung:} Bei Ladefehlern wird eine verständliche Meldung angezeigt, keine stillschweigend leere Seite.
\item \textbf{NFA-3 — Leerer Zustand:} Ohne Dokumente wird ein Hinweis angezeigt.
\item \textbf{NFA-4 — Konsistenz:} Bedienelemente orientieren sich an den übrigen Houston-Seiten.
\item \textbf{NFA-5 — Testbarkeit:} Der Kundenordner-Lookup ist durch Unit-Tests mit gemocktem \texttt{IAmazonS3} abgedeckt.
\item \textbf{NFA-6 — Pfadsicherheit:} Beim Download wird geprüft, dass der Schlüssel innerhalb des Kundenordners liegt.
\end{itemize}
+52 -1
View File
@@ -1,4 +1,55 @@
\section{Zeit- und Aufwandsplanung} \section{Zeit- und Aufwandsplanung}
\label{sec:schedule} \label{sec:schedule}
% TODO: Sprints 15–17.2026, Aufwandsschätzungen (Story Points), 24 Manntage / 192 h gesamt Für das Praktikum sind 24 Manntage (192 Stunden) vorgesehen. Der \textbf{offizielle Tätigkeitszeitraum} des Praktikums ist der 07.07.--25.08.2026; in diesem Fenster wurden die 24 Manntage erbracht. Der fachliche Rahmen des Vorhabens ist jedoch weiter gefasst und in drei Abschnitte zu trennen:
\begin{itemize}
\item \textbf{Vorlauf (Ende Mai bis 06.07.2026):} Themenfindung, Ausarbeitung des Feature~484 durch den Fachbereich und die Feature-Analyse vom 18.~Juni (Abschnitt~\ref{sec:feature-analysis}). Diese Klärung fand im regulären Tagesgeschäft statt und zählt \emph{nicht} zu den 24 Manntagen.
\item \textbf{Offizieller Praxiseinsatz (07.07.--25.08.2026):} Anforderungen, Backlog-Schnitt, Umsetzung der Sprints 15 und 16.2026 sowie der Abnahmetest.
\item \textbf{Nachlauf (ab 26.08.2026):} Fehlerbehebung aus der Abnahme, Benutzerhandbuch und die Produktivsetzung am 03.~September. Diese Arbeiten fielen in Sprint 17.2026 und liegen ebenfalls außerhalb der 24 Manntage; sie werden dokumentiert, weil sie den Auslieferungsstand des Moduls belegen.
\end{itemize}
Abbildung~\ref{fig:gantt} zeigt die Zeitplanung über alle drei Abschnitte.
\begin{figure}[H]
\centering
\includegraphics[width=\textwidth]{figures/diagrams/gantt-plan.pdf}
\caption{Zeitplanung des Projekts}
\label{fig:gantt}
\end{figure}
Vier Phasen gliedern die Planung: die \textbf{Analyse- und Konzeptionsphase} (Ende Mai bis Anfang Juli, überwiegend im Vorlauf) mit Themenfindung, Feature-Analyse und Backlog-Schnitt; die \textbf{Implementierungsphase} ab 27.~Juli über die Sprints 15 und 16; die parallele \textbf{Qualitätssicherung} (Code-Reviews, Abnahmetest ab 13.~August); und die \textbf{Abschlussphase} in Sprint 17.2026 mit Fehlerbehebung, Benutzerdokumentation und Produktivsetzung am 3.~September. Die Planung hing von der Infrastrukturbereitstellung ab — der Document Explorer konnte erst nach Verfügbarkeit des S3-Speichers umgesetzt werden; \emph{Stephan Janßen} vermerkte diese Abhängigkeit am 8.~Juli am Item.
Der Soll-Ist-Vergleich der Zeitplanung findet sich in Abschnitt~\ref{sec:target-comparison}.
\subsection{Verteilung der Manntage}
\label{sec:effort-distribution}
Der Praxiseinsatz war kein Vollzeitprojekt: Innerhalb des Tätigkeitszeitraums war ich parallel in das reguläre Tagesgeschäft eingebunden — Wartung bestehender Houston-Module, Support-Tickets, Code-Reviews fremder Pull Requests sowie Sprint-Zeremonien. Die 24 Manntage verteilen sich daher ungleichmäßig über die 36 Arbeitstage des Zeitraums. Tabelle~\ref{tab:manntage} weist die kalenderwöchentliche Verteilung aus.
\begin{longtable}{@{} l p{28mm} c c p{45mm} @{}}
\caption{Verteilung der 24 Manntage über den Tätigkeitszeitraum}
\label{tab:manntage} \\
\toprule
\textbf{KW} & \textbf{Zeitraum} & \textbf{AT} & \textbf{MT} & \textbf{Schwerpunkt im Projekt} \\
\midrule
\endfirsthead
\toprule
\textbf{KW} & \textbf{Zeitraum} & \textbf{AT} & \textbf{MT} & \textbf{Schwerpunkt im Projekt} \\
\midrule
\endhead
\bottomrule
\endfoot
28 & 07.--10.07. & 4 & 3 & Anforderungen, Backlog-Schnitt \\
29 & 13.--17.07. & 5 & 2 & Verfeinerung der Backlog Items \\
30 & 20.--24.07. & 5 & 2 & Infrastrukturbeantragung, Einarbeitung S3 \\
31 & 27.--31.07. & 5 & 4 & Document Explorer, S3-Client \\
32 & 03.--07.08. & 5 & 4 & Typ-Icons, Suche \\
33 & 10.--14.08. & 5 & 3 & Downloads, PDF-Vorschau, Abnahmebeginn \\
34 & 17.--21.08. & 5 & 4 & Typfilter, Paginierung, Ordner-Lookup \\
35 & 24.--25.08. & 2 & 2 & Abnahmebegleitung, Code-Reviews \\
\midrule
\textbf{Summe} & 07.07.--25.08. & \textbf{36} & \textbf{24} & \\
\end{longtable}
Die geringere Belastung in den Kalenderwochen 29 und 30 hat zwei Gründe: Zum einen war der S3-Speicher bis zum 27.~Juli nicht verfügbar, sodass die Implementierung nicht beginnen konnte (Abschnitt~\ref{sec:infrastructure}); zum anderen liefen in diesen Wochen Aufgaben aus anderen Houston-Modulen weiter. Die verlorene Kapazität wurde in den Kalenderwochen 31, 32 und 34 aufgeholt, in denen das Projekt nahezu die gesamte Arbeitszeit beanspruchte.
+15 -2
View File
@@ -1,5 +1,18 @@
\section{Abnahmetest} \section{Abnahmetest}
\label{sec:acceptance-testing} \label{sec:acceptance-testing}
% TODO: Maria-Lena Andersz — Sichtbarkeitsproblem des Menüpunkts (2026-08-13) \subsection{Ablauf}
% Rollenzuweisung klären, Bug 10134 (Suchfunktion), finale Abnahme 2026-08-24/25
Die fachliche Abnahme übernahm \emph{Maria-Lena Andersz}. Sie begann am 13.~August 2026 und zog sich mit Unterbrechungen bis zum 25.~August; geprüft wurde gegen die Akzeptanzkriterien der Backlog Items auf der Testumgebung. Die Abnahme zerfiel in zwei Abschnitte: zunächst Zugriffs- und Konfigurationsprobleme, erst danach die funktionale Prüfung.
\subsection{Zugriff und Konfiguration}
Der Menüpunkt „Dokumente" erschien nicht — Ursache war nicht der Code, sondern die Berechtigungsvergabe: der Testerin war die Anwendungsrolle nicht zugewiesen, das Verhalten entsprach der Spezifikation (Abschnitt~\ref{sec:authorization}). Die Rolle war Ende Juli nur für die Entwicklungsumgebung beantragt worden; dass ihre Vergabe an Testende ein eigener Schritt ist, war nirgends festgehalten. Hier bewährte sich die 403-Antwort: Bei 404 wäre nicht unterscheidbar gewesen, ob die Seite fehlt oder die Berechtigung.
Nach Klärung wurden keine Dokumente angezeigt; auch das lag an der Umgebung: fehlende Testdateien und eine falsche Zuordnung zwischen Testkonto und Organisation. Ob ein Benutzer Dokumente sieht, hängt von einer Kette ab — Anmeldetoken, Organisationszuordnung, Metadatum am Kundenordner; ist ein Glied falsch, zeigt die Seite keine Dokumente, aber auch keinen Fehler. Eine Prüfliste für die Inbetriebnahme hätte diesen Abschnitt verkürzt.
\subsection{Funktionale Prüfung}
Am 24.~August fand die funktionale Abnahme statt: Paginierung, Suchverhalten, Sprungmarken der Freigabelinks, Fehlerverhalten, Verknüpfungsdateien und Download. Sie verlief überwiegend erfolgreich; einzelne Bestandteile waren noch nicht verfügbar, da der Pull Request zur Ordnerverwaltung erst am 26.~August zusammengeführt wurde (Abschnitt~\ref{sec:folder-management}).
Ein Fehlerbericht: Das Suchfeld blendet nach Eingabe eine Schaltfläche zum Leeren ein, die auf den übrigen Houston-Seiten nicht existiert und damit NFA-4 (Konsistenz) verletzt. Weder Unit-Test noch Code-Review hätten ihn finden können; sichtbar wird die Abweichung erst beim Vergleich mit den bestehenden Seiten. Die Behebung — Entfernen der Schaltfläche — wurde am 27.~August über Pull Request 2206 zusammengeführt. Damit war der Dokumentenbereich vollständig abgenommen; sämtliche Backlog Items standen auf \texttt{Test Completed}.
+15 -4
View File
@@ -1,7 +1,18 @@
\section{Code-Reviews} \section{Code-Reviews}
\label{sec:code-reviews} \label{sec:code-reviews}
% TODO: 11 PRs (2154–2196), Reviewer: Timo Walter, Sarah Hinzmann, Robin Noack \subsection{Umfang und Prüftiefe}
% Reviewfunde: zentrale Auth über Program.cs, keyed Settings, 403 statt 404,
% Slug-Naming-Constraints (IBM-Doku), Fehlerbehandlung statt stiller leerer Seite Die Umsetzung verteilte sich auf dreizehn Pull Requests mit 114 Diskussionssträngen (Tabelle~\ref{tab:pull-requests} im Anhang). Kein Pull Request wurde abgelehnt; dass keiner verworfen wurde, geht auf die vorgelagerte Klärung durch Feature-Analyse und Backlog-Durchsicht zurück. Die Verteilung der Stränge folgte dem Risiko: die intensivste Prüfung erfuhren Einzeldownload (30 Stränge) und Freigabelinks (23), gefolgt vom Document Explorer (21); die PDF-Vorschau als reine Darstellungsfunktion kam mit zwei aus.
% Tabelle~\ref{tab:pull-requests} im Anhang
\subsection{Wiederkehrende Themen}
Über alle Diskussionsstränge hinweg lassen sich fünf Muster erkennen. \textbf{Sicherheit und Eingabeprüfung} machte den größten Anteil aus: Pfadprüfung beim Download, zeitlich begrenzte Zugriffs-URLs statt Dateiströme, Ausschluss benutzerdefinierter Symbole aus der Vorschau und keine Auslieferung über nicht authentifizierte Pfade. \textbf{Kompatibilität vor Ideallösung}: Mehrfach wurde eine sauberere Lösung zugunsten einer verlässlicheren verworfen — etwa Vorschaubilder als Rastergrafik, weil die Vektorvariante von verbreiteten Messengern nicht zuverlässig dargestellt wird (Abschnitt~\ref{sec:pdf-preview}). \textbf{Abgrenzung statt Ausweitung}: Erkannte Probleme wurden als neue Backlog Items erfasst statt im laufenden Pull Request miterledigt. \textbf{Struktur und Wartbarkeit}: auszulagernde Skripte, fehlertolerantes Auswerten von Aufzählungswerten, überflüssige Kommentare, uneinheitliche Benennungen. \textbf{Fachliche Klärung im Review}: In mehreren Fällen änderte die Diskussion nicht den Code, sondern das Backlog Item — etwa den Statuscode bei fehlender Berechtigung (403 statt 404).
\subsection{Wechsel der Reviewer}
Der Hauptreviewer wechselte zweimal: \emph{Timo Walter} prüfte die fünf Pull Requests der Anfangsphase, \emph{Sarah Hinzmann} übernahm Anfang August, \emph{Robin Noack} die Schlussphase; \emph{Hanna Ebner} beteiligte sich an den frühen Diskussionen. Der Wechsel hatte einen fachlichen Effekt: frühe Reviews befassten sich mit Architektur und Sicherheit, späte mit Benennung und Codestil; zugleich verteilte er das Wissen über das Modul im Team.
\subsection{Kritische Betrachtung des Vorgehens}
Am 27.~Juli wurden vier Pull Requests am selben Tag eröffnet, ein fünfter folgte tags darauf. Da sie inhaltlich aufeinander aufbauten (Abschnitt~\ref{sec:architecture}), ließ sich nur der erste zeitnah abschließen; die übrigen blieben zwei bis drei Wochen offen, und der überwiegende Teil der Zusammenführungen fiel in die Woche vom 18.~bis 21.~August — unmittelbar vor der Abnahme. Sequenzielles Vorgehen wäre vorzuziehen gewesen, da die inhaltliche Abhängigkeit echtes paralleles Vorankommen ohnehin verhinderte.
+16 -3
View File
@@ -1,5 +1,18 @@
\section{Releases} \section{Auslieferung}
\label{sec:releases} \label{sec:releases}
% TODO: DEV / TEST / PROD je eigener Bucket, Rollenvergabe vor Release \subsection{Umgebungen und Rollen}
% PR 2196 noch aktiv bei Dokumentationsabgabe
Die Auslieferung folgt dem in Houston etablierten dreistufigen Weg über Entwicklungs-, Test- und Produktivumgebung. Jede Stufe besitzt einen eigenen Speicher (Abschnitt~\ref{sec:s3-client}), der eingerichtet, erreichbar und korrekt hinterlegt sein muss; deshalb wurde die Infrastrukturbereitstellung (Abschnitt~\ref{sec:infrastructure}) bereits im Approval-Termin als Voraussetzung vermerkt.
Auch die Anwendungsrolle muss je Umgebung vorhanden und zugewiesen sein — eine begleitende Maßnahme, die im Abnahmetest zu Verzögerungen führte (Abschnitt~\ref{sec:acceptance-testing}). Vor der Produktivsetzung wurde daher am 20.~August geprüft, ob sämtliche Einstiegspunkte des Moduls hinter der Rolle liegen; die zentrale Registrierung (Abschnitt~\ref{sec:architecture}) macht eine ungeschützte Route unwahrscheinlich, die Prüfung bestätigte dies.
\subsection{Benutzerdokumentation}
Zum Funktionsumfang gehört das als PDF ausgelieferte Houston-Benutzerhandbuch, das für den Dokumentenbereich um ein eigenes Kapitel ergänzt wurde: Aufruf der Seite, Typfilter und Suche, Einzel- und Archivdownload, PDF-Vorschau, Freigabelinks sowie Verknüpfungsdateien. Erfasst als eigenes Backlog Item (10167) und über Pull Request 2215 am 2.~September zusammengeführt. Die Trennung von der Implementierung ist bewusst: Das Handbuch beschreibt den Endzustand und ließ sich sinnvoll erst schreiben, als sämtliche Funktionen zusammengeführt waren.
\subsection{Stand bei Produktivsetzung}
Alle dreizehn Pull Requests des Features sind zusammengeführt. Die Kernfunktionen entstanden in der Woche vom 18.~bis 21.~August; die Ordnerverwaltung mit dem neuen Kundenordner-Lookup folgte am 26.~August (Pull Request 2196), der Fehler aus der Abnahme wurde am 27.~August behoben (Pull Request 2206). Sämtliche Backlog Items stehen im Zustand \texttt{Test Completed} oder \texttt{Done}, die Recherche zur Speicherabfrage (9857) ist abgeschlossen. Am 3.~September wurde der Dokumentenbereich in die Produktivumgebung ausgeliefert.
Offen bleibt einzig das Backlog Item zur Nebenläufigkeit im verändernden Lookup-Pfad (10070). Es ist freigegeben, aber nicht eingeplant, da die Gegenmaßnahme von einer Rückfrage beim Betreiber des Speichers abhängt (Abschnitt~\ref{sec:race-conditions}).
+3 -1
View File
@@ -1,4 +1,6 @@
\section{Teststrategie} \section{Teststrategie}
\label{sec:test-strategy} \label{sec:test-strategy}
% TODO: Unit-Tests mit gemocktem IAmazonS3, Abnahmetests durch Maria-Lena Andersz Die Qualitätssicherung stützte sich auf drei Stufen. \textbf{Unit-Tests} sichern isoliert prüfbare Logik ab — vor allem Kundenordner-Auflösung und Pfadbehandlung — und laufen bei jedem Übersetzungsvorgang. \textbf{Code-Reviews} prüfen Entwurf, Lesbarkeit und Sicherheitseigenschaften, etwa ob eine Information auf einem nicht authentifizierten Pfad ausgeliefert werden darf. \textbf{Abnahmetests} prüfen gegen die Akzeptanzkriterien in der tatsächlichen Umgebung und erfassen Fehler aus dem Zusammenspiel von Konfiguration, Berechtigungen und realen Daten.
Die automatisierten Tests folgten dem Risiko, nicht einer Abdeckungsvorgabe: Für den Kundenordner-Lookup war Testabdeckung ausdrücklich als Akzeptanzkriterium aufgenommen, für die PDF-Vorschau nicht — ein Fehler im Lookup führt zur Anzeige fremder Dokumente oder Datenverlust und ist im Betrieb schwer zu bemerken, ein Fehler in der Vorschau ist beim ersten Aufruf offensichtlich. Die Kehrseite: Fehler in Bereichen ohne automatisierte Abdeckung wurden erst im Abnahmetest gefunden (Abschnitt~\ref{sec:acceptance-testing}).
+11 -3
View File
@@ -1,6 +1,14 @@
\section{Unit-Tests} \section{Unit-Tests}
\label{sec:unit-tests} \label{sec:unit-tests}
% TODO: DocumentsServiceTests — gemocktes IAmazonS3 \subsection{Testbarkeit durch Kapselung}
% Pfade: Fast-Path, Fallback, Rename, Create, Kollision, Fehlerfall
% Abbildung~\ref{fig:documents-service-tests} Da der Speicherzugriff über die SDK-Schnittstelle erfolgt, lässt sich diese in Tests durch eine Attrappe ersetzen; die Tests laufen ohne Netzwerkverbindung, Zugangsdaten und realen Speicher. Die Attrappe erlaubt zudem Zustände, die sich real kaum erzeugen ließen — etwa einen Kundenordner mit falschem Namen, aber korrektem Metadatum, oder einen Ordner ohne Metadatum.
\subsection{Abgedeckte Pfade}
Die Tests bilden die in Abschnitt~\ref{sec:lookup-decision} beschriebenen Wege einzeln ab: Direktzugriff, Kollisionsfall, Rückfallebene mit Umbenennung, Anlegen eines fehlenden Kundenordners und Ignorieren eines Ordners ohne gültiges Metadatum. Geprüft wird nicht nur das Ergebnis, sondern auch die Speicherzugriffe — beim Direktzugriff etwa, dass genau eine Metadatenabfrage und keine Auflistung stattfand. Denn die Anforderung war keine funktionale, sondern eine über den Aufwand: dass der Regelfall mit einem einzigen Zugriff auskommt, lässt sich nur über die beobachteten Aufrufe prüfen.
\subsection{Befunde aus dem Review von Testcode}
Zwei Reviewhinweise \emph{Timo Walters} betrafen die Tests selbst. Zum einen verwendeten Testdaten reale Kundennamen statt Platzhaltern; die Daten wurden ersetzt. Zum anderen bestand ein Test aus dem falschen Grund: Er brach bereits an einem unbekannten Typbezeichner ab, ohne die zu prüfende Sicherheitseigenschaft — dass ein in die Adresse geschriebener Kundenname kein Ergebnis liefert — je zu erreichen. Der Fund war nur durch Lesen zu erzielen, da der Test grün war. Als Konsequenz wurde die Anforderung, dass ein Test ohne den jeweiligen Fix fehlschlagen muss, in die Akzeptanzkriterien des Folgeitems zur Nebenläufigkeit aufgenommen (Abschnitt~\ref{sec:race-conditions}).
+25
View File
@@ -0,0 +1,25 @@
// Houston/Auth/HoustonRoles.cs
public static readonly string DocumentsView = "Documents.View";
// Houston/Program.cs
var viewDocumentsPolicy = "CanViewDocuments";
builder.Services.AddAuthorization(o =>
{
o.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
// ...
o.AddPolicy(viewDocumentsPolicy, policy => policy.RequireRole(HoustonRoles.DocumentsView));
});
builder.Services.AddRazorPages(o =>
{
// ...
o.Conventions.AllowAnonymousToPage("/Document/Share");
o.Conventions.AuthorizeFolder("/Document", viewDocumentsPolicy);
o.Conventions.AuthorizePage("/Documents", viewDocumentsPolicy);
});
// Houston/Services registrations
builder.Services.AddOptions<DocumentsSettings>().BindConfiguration("Documents");
builder.Services.AddDocumentsClient();
builder.Services.AddScoped<DocumentsService>();
+42
View File
@@ -0,0 +1,42 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Microsoft.AspNetCore.WebUtilities;
namespace Houston.Model.Documents;
/// <summary>
/// Public URL id of a customer document.
/// </summary>
/// <remarks>
/// The identifier used in URLs (e.g. <c>/document/{id}/download</c> or <c>/document/{id}/share</c>)
/// is the URL base64 encoding of the document path <em>without</em> the customer/organization
/// folder. For an object stored at <c>OrgX/TypeY/FileZ.pdf</c> the id therefore encodes
/// <c>TypeY/FileZ.pdf</c>. The organization part is never part of the id, so it can only ever be
/// derived server side from the authenticated user.
/// </remarks>
public readonly record struct DocumentId(string Value)
{
public static DocumentId Encode(DocumentRelativePath relativePath)
=> new(WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(relativePath.ToString())));
public bool TryDecode([NotNullWhen(true)] out DocumentRelativePath? relativePath)
{
relativePath = null;
if (String.IsNullOrEmpty(Value))
return false;
try
{
var decoded = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Value));
return DocumentRelativePath.TryFromS3Key(decoded, out relativePath);
}
catch (FormatException)
{
relativePath = null;
return false;
}
}
public override string ToString() => Value;
}
+41
View File
@@ -0,0 +1,41 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace Houston.Model.Documents;
/// <summary>
/// Full S3 object key of a customer document, including the customer/organization prefix.
/// </summary>
public readonly record struct DocumentKey(OrgSlug Org, DocumentType? Type, DocumentName Name)
{
public static bool TryFromS3Key(string key, [NotNullWhen(true)] out DocumentKey? result)
{
result = null;
if (key.EndsWith('/')) return false;
var parts = key.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (parts.Contains("..")) return false;
result = parts switch
{
[var org, var name] when OrgSlug.TryFromFolderName(org, out var slug)
=> new(slug.Value, null, new(name)),
[var org, var type, var name] when OrgSlug.TryFromFolderName(org, out var slug)
=> new(slug.Value, DocumentTypeExtensions.ParseStorageFolderName(type), new(name)),
_ => null,
};
return result is not null;
}
public override string ToString()
{
StringBuilder result = new();
result.Append(Org.ToString());
if (Type is { } type) result.Append(type.StorageFolderName).Append('/');
result.Append(Name);
return result.ToString();
}
}
+27
View File
@@ -0,0 +1,27 @@
namespace Houston.Model.Documents;
public readonly record struct DocumentName(string Value)
{
public bool Matches(string pattern)
{
return Value.Contains(pattern, StringComparison.CurrentCultureIgnoreCase);
}
/// <summary>
/// Whether this document is a PDF and therefore eligible for the inline modal preview.
/// </summary>
public bool IsPdf => Value.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Whether this is an external document link (a .URL file).
/// </summary>
public bool IsLink => Value.EndsWith(".url", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// File display name.
/// </summary>
public string DisplayName => IsLink ? Value[..^".url".Length] : Value;
/// <inheritdoc />
public override string ToString() => Value;
}
+39
View File
@@ -0,0 +1,39 @@
private async IAsyncEnumerable<DocumentKey> SearchDocumentsAsync(
OrgSlug org,
string? searchTerm,
DocumentTypeSet documentTypes,
bool includeUntypedDocuments,
DocumentRelativePath? startAfter,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var query = searchTerm?.Trim();
await foreach (var doc in ListDocumentsAsync(org, startAfter, cancellationToken))
{
if (!doc.Type.MatchesFilter(documentTypes, includeUntypedDocuments))
continue;
if (!String.IsNullOrEmpty(query) && !doc.Name.Matches(query))
continue;
yield return doc;
}
}
// Houston/Model/Documents/DocumentName.cs
public bool Matches(string pattern)
{
return Value.Contains(pattern, StringComparison.CurrentCultureIgnoreCase);
}
// Houston/Model/Documents/DocumentType.cs
public bool MatchesFilter(DocumentTypeSet documentTypes, bool includeUntypedDocuments)
{
if (documentTypes.IsEmpty && !includeUntypedDocuments)
return true;
if (documentType is null)
return includeUntypedDocuments;
return documentTypes.Types.Contains(documentType.Value);
}
+42
View File
@@ -0,0 +1,42 @@
using System.Globalization;
using Houston.Resources;
namespace Houston.Model.Documents;
public enum DocumentType
{
ServiceProtocol,
AcceptanceDocuments,
SlaReportsTicketProcessing,
MonitoringReports,
SecurityAssessments,
BillingData,
ContractDocuments,
}
public static class DocumentTypeExtensions
{
public const string OtherDocumentsDisplayName = "Sonstige Dokumente";
public const string OtherDocumentsValue = "OtherDocuments";
public static string? FromStorageFolderName(string storageFolderName) =>
APITranslation.ResourceManager.GetString(storageFolderName, CultureInfo.InvariantCulture);
public static DocumentType? ParseStorageFolderName(string storageFolderName)
{
var type = FromStorageFolderName(storageFolderName);
if (!Enum.TryParse<DocumentType>(type, ignoreCase: true, out var variant))
return null;
return variant;
}
extension(DocumentType documentType)
{
/// <summary>
/// The name of the S3 subfolder that holds documents of this type, i.e. the German translation.
/// </summary>
public string StorageFolderName =>
APITranslation.ResourceManager.GetString(documentType.ToString(), CultureInfo.InvariantCulture)
?? throw new NotImplementedException($"Missing translation for DocumentType '{documentType}'.");
}
+65
View File
@@ -0,0 +1,65 @@
<tbody>
@foreach (var file in Model.Files)
{
<tr class="document-row" id="@Documents.DocumentViewId(file.Id)">
<td>
@if (Documents.IsDownloadable(file.Name))
{
<div class="form-check mb-0">
<input type="checkbox" name="selected" value="@file.Id"
class="form-check-input js-document-select"
aria-label="@file.Name.DisplayName auswählen"/>
</div>
}
</td>
<td>
<i class="doc-type-icon bx-sm @file.IconClass" title="@file.Type.DisplayName"></i>
</td>
<td>@file.Name.DisplayName</td>
<td class="text-end">
<div class="d-flex">
@if (file.Name.IsPdf)
{
<button type="button"
class="btn btn-icon btn-text-secondary rounded-pill js-document-preview"
data-preview-url="@Model.DocumentPreviewUrl(file.Id)"
data-document-name="@file.Name.DisplayName"
title="@file.Name.DisplayName als Vorschau öffnen"
aria-label="@file.Name.DisplayName als Vorschau öffnen">
<i class="bx bx-show-alt"></i>
</button>
}
else
{
<span class="btn btn-icon rounded-pill invisible" aria-hidden="true">
<i class="bx bx-show-alt"></i>
</span>
}
<button type="button"
class="btn btn-icon btn-text-secondary rounded-pill js-document-share"
data-share-url="@Model.DocumentShareUrl(file.Id)"
title="Link zu @file.Name.DisplayName kopieren"
aria-label="Link zu @file.Name.DisplayName kopieren">
<i class="bx bx-share-alt"></i>
</button>
@if (file.Name.IsLink)
{
<a class="btn btn-icon btn-text-secondary rounded-pill"
asp-page="/Document/Download" asp-route-id="@file.Id"
target="_blank" rel="noopener noreferrer"
title="@file.Name.DisplayName in neuem Tab öffnen" aria-label="@file.Name.DisplayName in neuem Tab öffnen">
<i class="bx bx-download"></i>
</a>
}
else
{
<a class="btn btn-icon btn-text-secondary rounded-pill"
asp-page="/Document/Download" asp-route-id="@file.Id"
title="@file.Name.DisplayName herunterladen" aria-label="@file.Name.DisplayName herunterladen">
<i class="bx bx-download"></i>
</a>
}
</div>
</td>
</tr>
}
+49
View File
@@ -0,0 +1,49 @@
namespace Houston.Services;
public class IniParser
{
public async Task<IniDocument> ParseAsync(TextReader reader, CancellationToken cancellationToken)
{
var sections = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
var current = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
sections[String.Empty] = current;
while (await reader.ReadLineAsync(cancellationToken) is { } line)
{
var trimmed = line.Trim();
// Skip blank lines and comments
if (trimmed.Length == 0 || trimmed[0] is ';' or '#')
continue;
if (trimmed.StartsWith('[') && trimmed.EndsWith(']'))
{
var name = trimmed[1..^1].Trim();
if (!sections.TryGetValue(name, out current!))
sections[name] = current = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
continue;
}
var separator = trimmed.IndexOf('=');
if (separator < 0)
continue;
var key = trimmed[..separator].Trim();
if (key.Length == 0)
continue;
// Last value wins for duplicate keys within a section.
current[key] = trimmed[(separator + 1)..].Trim();
}
return new(sections);
}
/// <summary>
/// Parses INI content from a string.
/// </summary>
public Task<IniDocument> ParseAsync(string content, CancellationToken cancellationToken)
{
using var reader = new StringReader(content);
return ParseAsync(reader, cancellationToken);
}
+25
View File
@@ -0,0 +1,25 @@
private async IAsyncEnumerable<DocumentKey> ListDocumentsAsync(
OrgSlug org,
DocumentRelativePath? startAfter,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var isFirstRequest = true;
var requestBuilder = () =>
{
var r = new ListObjectsV2Request
{
Prefix = org.ToString(),
StartAfter = isFirstRequest && startAfter is { } doc
? new DocumentKey(org, doc.Type, doc.Name).ToString()
: null,
};
isFirstRequest = false;
return r;
};
await foreach (var obj in EnumerateObjectsAsync(requestBuilder, cancellationToken))
if (DocumentKey.TryFromS3Key(obj.Key, out var key))
yield return key.Value;
}
+112
View File
@@ -0,0 +1,112 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace Houston.Model.Documents;
public readonly record struct OrgSlug
{
public const int MaxLength = 128;
private const string CharactersToReplace = @"/\{}^%`][""><~#|";
private const char ReplacementCharacter = '-';
/// <summary>
/// Characters that must not start or end a folder name, because they are either invisible or
/// break path semantics on the client side.
/// </summary>
private static readonly char[] UntrimmableEdgeCharacters = [' ', '.', ReplacementCharacter];
public string Value { get; }
private OrgSlug(string value) => Value = value;
public static bool TryFromS3Key(string key, [NotNullWhen(true)] out OrgSlug? result)
{
result = null;
if (!key.EndsWith('/')) return false;
var parts = key.Split('/', StringSplitOptions.RemoveEmptyEntries);
return parts is [var name] && TryFromFolderName(name, out result);
}
public static bool TryFromFolderName(string name, [NotNullWhen(true)] out OrgSlug? result)
{
result = null;
if (String.IsNullOrWhiteSpace(name)) return false;
if (name.Contains('/')) return false;
if (name == "..") return false;
result = new OrgSlug(name);
return true;
}
/// <summary>
/// Builds the canonical folder name for an organization.
/// </summary>
/// <returns>
/// <c>false</c> if the name contains nothing that survives the normalization, in which case
/// there is no folder name Houston could safely claim.
/// </returns>
public static bool TryFrom(OrgName name, [NotNullWhen(true)] out OrgSlug? result)
{
result = null;
var slug = Normalize(name.Value, MaxLength);
if (slug.Length == 0)
return false;
result = new OrgSlug(slug);
return true;
}
public static bool TryFrom(OrgName name, OrgId id, [NotNullWhen(true)] out OrgSlug? result)
{
result = null;
var normalizedId = Normalize(id.Value, MaxLength);
if (normalizedId.Length == 0)
return false;
// The id is what makes the folder name unique, so the name is what gets truncated.
var suffix = $" ({normalizedId})";
if (suffix.Length >= MaxLength)
return false;
var slug = Normalize(name.Value, MaxLength - suffix.Length);
result = new OrgSlug(slug.Length == 0 ? normalizedId : slug + suffix);
return true;
}
private static string Normalize(string value, int maxLength)
{
var builder = new StringBuilder(Math.Min(value.Length, maxLength));
foreach (var character in value.Trim().TakeWhile(c => builder.Length != maxLength))
{
builder.Append(Char.IsControl(character) || CharactersToReplace.Contains(character)
? ReplacementCharacter
: character);
}
// Truncating can uncover a trailing space or dot, so trimming happens afterwards.
return builder.ToString().Trim().Trim(UntrimmableEdgeCharacters).Trim();
}
/// <summary>
/// S3 prefix of the folder, including the trailing slash.
/// </summary>
public override string ToString()
{
// A default(OrgSlug) would resolve to the bucket root and expose every customer's
// documents, so it must never be used as a prefix.
if (String.IsNullOrEmpty(Value))
throw new InvalidOperationException("An uninitialized OrgSlug cannot be used as an S3 prefix.");
return Value + '/';
}
}
@@ -0,0 +1,28 @@
private async IAsyncEnumerable<ListObjectsV2Response> EnumerateResponsesAsync(
Func<ListObjectsV2Request> requestBuilder,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
string? continuationToken = null;
do
{
var request = requestBuilder();
request.ContinuationToken = continuationToken;
request.BucketName = _settings.S3.Bucket;
var response = await s3.ListObjectsV2Async(request, cancellationToken);
yield return response;
continuationToken = response.IsTruncated == true ? response.NextContinuationToken : null;
} while (continuationToken is not null);
}
private async IAsyncEnumerable<S3Object> EnumerateObjectsAsync(
Func<ListObjectsV2Request> requestBuilder,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var response in EnumerateResponsesAsync(requestBuilder, cancellationToken))
{
foreach (var obj in response.S3Objects ?? [])
yield return obj;
}
}
+39
View File
@@ -0,0 +1,39 @@
private async Task<string?> GetPreSignedUrlAsync(DocumentKey key, bool inline, CancellationToken cancellationToken)
{
if (!await ObjectExistsAsync(key, cancellationToken))
return null;
var request = new GetPreSignedUrlRequest
{
BucketName = _settings.S3.Bucket,
Key = key.ToString(),
Verb = HttpVerb.GET,
Expires = DateTime.UtcNow.Add(DownloadUrlLifetime),
ResponseHeaderOverrides =
{
ContentDisposition = inline
? InlineContentDisposition(key.Name.ToString())
: AttachmentContentDisposition(key.Name.ToString()),
},
};
return await s3.GetPreSignedURLAsync(request);
}
private static string AttachmentContentDisposition(string fileName)
{
var header = new ContentDispositionHeaderValue("attachment");
header.SetHttpFileName(fileName);
return header.ToString();
}
/// <summary>
/// Builds a <c>Content-Disposition</c> header that lets the browser render the document inline
/// while preserving the file name, using RFC 5987 encoding so non ASCII file names survive.
/// </summary>
private static string InlineContentDisposition(string fileName)
{
var asciiFallback = new string(fileName.Select(c => c is >= ' ' and < (char)127 and not '"' ? c : '_').ToArray());
var encoded = Uri.EscapeDataString(fileName);
return $"inline; filename=\"{asciiFallback}\"; filename*=UTF-8''{encoded}";
}
+54
View File
@@ -0,0 +1,54 @@
/// <summary>
/// todo: this function can currently cause dataloss. see #10070
/// </summary>
/// <returns>success</returns>
private async Task<bool> RenameCustomerFolderAsync(
OrgSlug source, OrgSlug destination, OrgId owner, CancellationToken cancellationToken)
{
if (source == destination)
return true;
if (!await CanClaimCustomerFolderAsync(destination, owner, cancellationToken))
return false;
var sourcePrefix = source.ToString();
var destinationPrefix = destination.ToString();
var sourceKeys = await ListKeysAsync(sourcePrefix, cancellationToken);
// The marker of the customer folder carries the efecte-org-id, so it is copied last and
// deleted first: the destination only becomes resolvable once it is complete. Everything
// below the customer folder keeps its relative key, including subfolders Houston does not
// know, so no document ever ends up somewhere else.
var documentKeys = sourceKeys.Where(key => key != sourcePrefix).ToList();
var markerKeys = sourceKeys.Where(key => key == sourcePrefix).ToList();
var copiedKeys = new List<string>(sourceKeys.Count);
try
{
foreach (var key in documentKeys.Concat(markerKeys))
{
var destinationKey = destinationPrefix + key[sourcePrefix.Length..];
await CopyObjectAsync(key, destinationKey, cancellationToken);
copiedKeys.Add(destinationKey);
}
}
catch (AmazonS3Exception exception)
{
ExceptionlessClient.Default.SubmitLog(
$"Could not move customer folder '{source}' to '{destination}': {exception.Message}. "
+ "The folder stays where it is.", LogLevel.Error);
// Leftover copies would block every later attempt, because the destination is only
// claimed while it is empty. Cleaning them up keeps the move retryable.
await TryDeleteObjectsAsync(copiedKeys, cancellationToken);
return false;
}
// From here on the destination is authoritative. A failure while deleting only leaves
// garbage behind, the customer already sees all of his documents under the new name.
await TryDeleteObjectsAsync(documentKeys, cancellationToken);
await TryDeleteObjectsAsync(markerKeys, cancellationToken);
return true;
}
+64
View File
@@ -0,0 +1,64 @@
private async Task<OrgSlug?> GetCustomerFolderAsync(OrgName name, OrgId id, CancellationToken cancellationToken)
{
if (!OrgSlug.TryFrom(name, out var canonicalSlug) || !OrgSlug.TryFrom(name, id, out var collisionSlug))
{
ExceptionlessClient.Default.SubmitLog($"Efecte name '{name}' of organization {id} does not yield a usable folder name.", LogLevel.Warn);
return null;
}
var (prettySlug, uniqueSlug) = (canonicalSlug.Value, collisionSlug.Value);
// Fast path: pretty slug exists & meta-id matches => return slug(name)
var prettyState = await InspectCustomerFolderAsync(prettySlug, id, cancellationToken);
if (prettyState is CustomerFolderState.Owned)
return prettySlug;
// Conflict resolution: pretty slug exists & meta-id does not match => return slug(name,id)
var uniqueState = await InspectCustomerFolderAsync(uniqueSlug, id, cancellationToken);
if (uniqueState is CustomerFolderState.Owned)
return uniqueSlug;
// State cleanup:
// Either the folder does not have the name it should have or doesn't exist, so the bucket has to be scanned. And
// if necessary and possible, renamed such that the next lookup hits one of the expected slugs.
OrgSlug? availableSlug =
prettyState is CustomerFolderState.Missing ? prettySlug
: uniqueState is CustomerFolderState.Missing ? uniqueSlug
: null;
var currentSlug = await SearchCustomerPrefixAsync(id, cancellationToken);
if (currentSlug is null)
{
if (availableSlug is not { } available)
{
ExceptionlessClient.Default.SubmitLog(
$"Organization {id} has no customer folder and neither "
+ $"'{prettySlug}' nor '{uniqueSlug}' is available.", LogLevel.Error);
return null;
}
if (!await CreateCustomerFolderAsync(available, id, cancellationToken))
return null;
if (await InspectCustomerFolderAsync(available, id, cancellationToken) is not CustomerFolderState.Owned)
{
ExceptionlessClient.Default.SubmitLog(
$"Organization {id} has no customer folder and "
+ $"'{available}' could not be created.", LogLevel.Error);
return null;
}
return available;
}
if (availableSlug is null || availableSlug.Value == currentSlug.Value)
return currentSlug;
// A failed move leaves every document under the name it already had, so that name is what
// gets handed back. The next lookup simply tries the move again.
return await RenameCustomerFolderAsync(currentSlug.Value, availableSlug.Value, id, cancellationToken)
? availableSlug
: currentSlug;
}
+38
View File
@@ -0,0 +1,38 @@
using Amazon.Runtime;
using Amazon.S3;
using Houston.Settings;
using Microsoft.Extensions.Options;
namespace Houston.Extensions;
public static class DocumentsExtensions
{
extension(IServiceCollection services)
{
public IServiceCollection AddDocumentsClient()
{
return services.AddKeyedScoped<IAmazonS3>("Documents", (services, _) =>
{
var settings = services.GetRequiredService<IOptions<DocumentsSettings>>();
var client = new AmazonS3Client(
new BasicAWSCredentials(settings.Value.S3.Key, settings.Value.S3.Secret),
new AmazonS3Config { ServiceURL = settings.Value.S3.Endpoint });
// The SDK percent-encodes the separators in x-amz-copy-source ("bucket%2Fkey%2Fdoc.pdf").
// AWS decodes that again, our S3 splits bucket and key on the first literal slash and ends up
// without a key ("Invalid copy source object key"), so the separators are restored here. The
// event runs before the signer, hence the corrected value is the one that gets signed.
client.BeforeRequestEvent += (_, args) =>
{
if (args is WebServiceRequestEventArgs { Headers: { } headers }
&& headers.TryGetValue("x-amz-copy-source", out var copySource)
&& copySource.Contains("%2F", StringComparison.Ordinal))
{
headers["x-amz-copy-source"] = copySource.Replace("%2F", "/", StringComparison.Ordinal);
}
};
return client;
});
}
}
+16
View File
@@ -0,0 +1,16 @@
namespace Houston.Settings;
public class DocumentsSettings
{
public required S3Settings S3 { get; set; }
}
namespace Houston.Settings;
public class S3Settings
{
public required string Endpoint { get; set; }
public required string Key { get; set; }
public required string Secret { get; set; }
public required string Bucket { get; set; }
}
+61
View File
@@ -0,0 +1,61 @@
using Houston.Model.Documents;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Houston.Pages.Document;
/// <summary>
/// Anonymous landing page for shared document links. It exposes only metadata derived from the
/// document id and never fetches or renders file contents.
/// </summary>
public class Share : PageModel
{
[BindProperty(SupportsGet = true)]
public string Id { get; set; } = String.Empty;
public string Title { get; private set; } = "Dokument";
public string ImageUrl { get; private set; } = String.Empty;
public string ExplorerUrl { get; private set; } = String.Empty;
public IActionResult OnGet()
{
var docId = new DocumentId(Id);
if (!docId.TryDecode(out var relativePath))
return NotFound();
var type = relativePath.Value.Type;
var name = relativePath.Value.Name;
var iconPath = type.PreviewPath;
Title = name.DisplayName;
ImageUrl = AbsoluteUrl(iconPath);
ExplorerUrl = AbsoluteUrl("/documents", BuildExplorerQuery(type, name), $"#{Documents.DocumentViewId(docId)}");
return Page();
}
private static QueryString BuildExplorerQuery(DocumentType? type, DocumentName name)
{
return new QueryBuilder {
{ "q", name.DisplayName },
{ "types", type.Name }
}.ToQueryString();
}
private string AbsoluteUrl(string path, QueryString query = default, string? fragment = null)
{
return UriHelper.BuildAbsolute(
Request.Scheme,
Request.Host,
Request.PathBase,
path,
query,
fragment: fragment is null ? default : new FragmentString(fragment)
);
}
}
+12
View File
@@ -0,0 +1,12 @@
[Theory]
[InlineData("../Kunde2/secret.pdf")]
[InlineData("sub/../../Kunde2/secret.pdf")]
public void DocumentIdTryParseRejectsPathTraversal(string relativePath)
{
Assert.False(new DocumentId(UnsafeDocumentIdValue(relativePath)).TryDecode(out _));
}
// Test-Hilfsfunktion: erzeugt bewusst eine ID, die die Validierung umgeht.
private static string UnsafeDocumentIdValue(string relativePath)
=> WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(relativePath));
+44
View File
@@ -0,0 +1,44 @@
/// <summary>
/// A bucket without a single object: every lookup answers with a 404 and every listing is empty
/// until a test puts something in via <see cref="GiveFolder"/>, <see cref="GiveTopLevelFolders"/>
/// or <see cref="GiveObjects"/>.
/// </summary>
private static IAmazonS3 EmptyBucket()
{
var s3 = Substitute.For<IAmazonS3>();
// Objects written during the test become visible to later lookups, just like in a real bucket.
var writtenObjects = new Dictionary<string, string?>(StringComparer.Ordinal);
s3.GetObjectMetadataAsync(Arg.Any<GetObjectMetadataRequest>(), Arg.Any<CancellationToken>())
.Returns(call =>
{
var key = call.Arg<GetObjectMetadataRequest>().Key;
if (!writtenObjects.TryGetValue(key, out var owner))
throw new AmazonS3Exception("not found") { StatusCode = System.Net.HttpStatusCode.NotFound };
return owner is null ? new GetObjectMetadataResponse() : MetadataResponse(owner);
});
s3.ListObjectsV2Async(Arg.Any<ListObjectsV2Request>(), Arg.Any<CancellationToken>())
.Returns(_ => new ListObjectsV2Response { IsTruncated = false });
s3.CopyObjectAsync(Arg.Any<CopyObjectRequest>(), Arg.Any<CancellationToken>())
.Returns(_ => new CopyObjectResponse());
s3.PutObjectAsync(Arg.Any<PutObjectRequest>(), Arg.Any<CancellationToken>())
.Returns(call =>
{
var request = call.Arg<PutObjectRequest>();
writtenObjects[request.Key] = request.Metadata["efecte-org-id"];
return new PutObjectResponse();
});
s3.DeleteObjectsAsync(Arg.Any<DeleteObjectsRequest>(), Arg.Any<CancellationToken>())
.Returns(_ => new DeleteObjectsResponse());
return s3;
}
+28
View File
@@ -0,0 +1,28 @@
.doc-type-icon {
display: inline-flex;
width: 1.25em;
height: 1.25em;
vertical-align: middle;
align-items: center;
justify-content: center;
}
@each $name in (
'service-protokoll',
'abnahme-dokumente',
'sla-reports',
'monitoring-reports',
'security-assessments',
'abrechnungsdaten',
'vertragsunterlagen',
'default',
'ms-sharepoint',
'ms-onedrive',
'ms-teams',
) {
.doc-type-icon-#{$name} {
background-color: currentColor;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
mask-image: url('/img/document-types/icons/#{$name}.svg');
+43
View File
@@ -0,0 +1,43 @@
public async Task<string?> TryGetUrlFileIconClassAsync(DocumentKey doc, CancellationToken cancellationToken)
{
var ini = await TryReadUrlFileAsync(doc, cancellationToken);
var iconFile = ini?.GetValue("InternetShortcut", "IconFile");
return String.IsNullOrEmpty(iconFile) ? null : iconFile;
}
private async Task<string?> TryGetUrlFileTargetAsync(DocumentKey doc, CancellationToken cancellationToken)
{
var ini = await TryReadUrlFileAsync(doc, cancellationToken);
var target = ini?.GetValue("InternetShortcut", "URL");
if (String.IsNullOrWhiteSpace(target))
return null;
if (!Uri.TryCreate(target, UriKind.Absolute, out var uri))
return null;
if (!(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
return null;
return uri.ToString();
}
private async Task<IniDocument?> TryReadUrlFileAsync(DocumentKey doc, CancellationToken cancellationToken)
{
try
{
using var response = await s3.GetObjectAsync(new GetObjectRequest
{
BucketName = _settings.S3.Bucket,
Key = doc.ToString(),
}, cancellationToken);
using var reader = new StreamReader(response.ResponseStream);
return await iniParser.ParseAsync(reader, cancellationToken);
}
catch (AmazonS3Exception exception) when (exception.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
}
+41
View File
@@ -0,0 +1,41 @@
/// <summary>
/// Streams the prepared documents into a ZIP archive written directly onto <paramref name="destination"/>
/// </summary>
public async Task WriteZipArchiveAsync(
OrgSlug orgSlug, IEnumerable<DocumentRelativePath> documents,
Stream destination, CancellationToken cancellationToken)
{
await using var zip = await ZipArchive.CreateAsync(
destination, ZipArchiveMode.Create, leaveOpen: true,
entryNameEncoding: null, cancellationToken);
foreach (var path in documents)
{
var key = new DocumentKey(orgSlug, path.Type, path.Name);
using var response = await TryGetObjectAsync(new GetObjectRequest
{
BucketName = _settings.S3.Bucket,
Key = key.ToString(),
}, cancellationToken);
if (response is null)
continue;
var entry = zip.CreateEntry(path.ToString(), CompressionLevel.Optimal);
await using var entryStream = await entry.OpenAsync(cancellationToken);
await response.ResponseStream.CopyToAsync(entryStream, cancellationToken);
}
}
private async Task<GetObjectResponse?> TryGetObjectAsync(GetObjectRequest request, CancellationToken cancellationToken)
{
try
{
return await s3.GetObjectAsync(request, cancellationToken);
}
catch (AmazonS3Exception exception) when (exception.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
}
+42
View File
@@ -0,0 +1,42 @@
public async Task<IActionResult> OnPostDownloadZipAsync(CancellationToken cancellationToken)
{
if (!OrgName.TryCreate(User.GetCompanyName(), out var orgName))
return NotFound();
var fallback = RedirectToPage(new
{
q = Search,
types = TypeFilterSelection,
pageSize = PageSize,
pageToken = PageToken,
});
if (SelectedDocuments is null || SelectedDocuments.Count == 0)
return fallback;
var relativePaths = SelectedDocuments
.SelectWhere<string, DocumentRelativePath>(x => new DocumentId(x).TryDecode(out var y) ? y.Value : null)
.Where(x => IsDownloadable(x.Name))
.ToList();
if (relativePaths.Count == 0)
return fallback;
var orgSlug = await documents.ResolveCustomerPrefixAsync(OrgId, orgName.Value, cancellationToken);
if (orgSlug is null)
return fallback;
// Zipping compresses each entry with a DeflateStream, which flushes its buffers synchronously
// when the entry is closed. Kestrel forbids synchronous response writes by default, so we opt
// in for this streamed response only. Nothing large is written synchronously - the document
// payloads are still copied with async IO.
var bodyControl = HttpContext.Features.Get<IHttpBodyControlFeature>();
bodyControl?.AllowSynchronousIO = true;
Response.ContentType = "application/zip";
Response.Headers.ContentDisposition = $"attachment; filename=\"{DocumentsService.ZipDownloadFileName}\"";
await documents.WriteZipArchiveAsync(orgSlug.Value, relativePaths, Response.Body, cancellationToken);
return new EmptyResult();
}
-10
View File
@@ -1,10 +0,0 @@
stateDiagram-v2
[*] --> New
New --> ToApprove : Linus reicht PBI ein
ToApprove --> Approved : Approval-Team
Approved --> Committed : Sprint-Planung
Committed --> DevCompleted : PR gemerged
DevCompleted --> TestCompleted : Abnahme bestanden
TestCompleted --> [*]
ToApprove --> Removed : Verworfen
Removed --> New : Wiederhergestellt
+15 -11
View File
@@ -1,18 +1,22 @@
sequenceDiagram sequenceDiagram
participant B as Browser autonumber
actor U as Benutzer
participant H as Houston participant H as Houston
participant AAD as Entra ID participant AAD as Entra ID
participant S3 as S3-Speicher participant S3 as S3-Speicher
B->>H: GET /documents U->>H: GET /documents
H->>H: Prüfe Claim Documents.Read H->>AAD: Authentifizierung (OpenID Connect)
alt Keine Rolle AAD-->>H: Token mit Claims:<br/>roles, efecte:company_id,<br/>efecte:company_name
H-->>B: 403 Forbidden
alt Rolle Documents.Read fehlt
H-->>U: 403 Forbidden<br/>(Menuepunkt bereits ausgeblendet)
else Rolle vorhanden else Rolle vorhanden
H->>H: Lese efecte:company_id aus Claim H->>H: orgId, orgName aus Claims lesen
H->>S3: ResolveCustomerPrefixAsync(orgId) H->>S3: ResolveCustomerPrefixAsync(orgId, orgName)
S3-->>H: Kundenordner-Prefix S3-->>H: Prefix des Kundenordners
H->>S3: ListObjectsV2(prefix) H->>S3: ListObjectsV2(prefix, delimiter)
S3-->>H: Objektliste S3-->>H: Objektliste des Kunden
H-->>B: Documents-Seite H->>H: Typ je Dokument aus<br/>Unterordner ableiten
H-->>U: Dokumentenliste
end end
Binary file not shown.
+10 -5
View File
@@ -1,8 +1,10 @@
gantt gantt
title Zeitplanung PidI 3 (Sprints 15–17.2026) title Zeitplanung PidI 3 (Sprints 15–17.2026)
dateFormat YYYY-MM-DD dateFormat YYYY-MM-DD
section Planung section Vorlauf
Feature-Analyse :done, 2026-06-18, 1d Feature-Analyse :done, 2026-06-18, 1d
section Sprint 14.2026
Praxiseinsatz Beginn :milestone, done, 2026-07-07, 0d
Anforderungen & Backlog :done, 2026-07-07, 5d Anforderungen & Backlog :done, 2026-07-07, 5d
section Sprint 15.2026 section Sprint 15.2026
Document Explorer :done, 2026-07-27, 5d Document Explorer :done, 2026-07-27, 5d
@@ -10,9 +12,12 @@ gantt
Downloads & PDF-Modal :done, 2026-08-15, 7d Downloads & PDF-Modal :done, 2026-08-15, 7d
section Sprint 16.2026 section Sprint 16.2026
Typfilter & Pagination :done, 2026-08-18, 4d Typfilter & Pagination :done, 2026-08-18, 4d
Ordner-Lookup :active, 2026-08-17, 8d Ordner-Lookup :done, 2026-08-17, 10d
section Sprint 17.2026 Praxiseinsatz Ende :milestone, done, 2026-08-25, 0d
Race Conditions (10070) :2026-08-26, 5d section Sprint 17.2026 (Nachlauf)
Bugfix Suchfeld (10134) :done, 2026-08-27, 1d
Handbuch (10167) :done, 2026-08-31, 3d
Race Conditions (10070) :2026-09-08, 5d
section Qualitätssicherung section Qualitätssicherung
Abnahmetest :done, 2026-08-13, 12d Abnahmetest :done, 2026-08-13, 12d
Release :2026-08-26, 3d Produktivsetzung :milestone, done, 2026-09-03, 0d
Binary file not shown.
+15 -9
View File
@@ -1,10 +1,16 @@
flowchart TD flowchart TD
A[ResolveCustomerPrefixAsync\norgId, orgName] --> B{Fast-Path:\nGetObjectMetadata\nslug-name/} A["ResolveCustomerPrefixAsync(orgId, orgName)"] --> B["Fast-Path:<br/>GetObjectMetadata auf<br/>slug(name)/"]
B -->|Gefunden & ID passt| Z[✓ Fertig\n1 Request] B -->|"Marker vorhanden<br/>und org-id passt"| Z1["Fertig — 1 Request"]
B -->|Nicht gefunden oder\nID passt nicht| C{Kollisions-Fast-Path:\nslug-name orgId /} B -->|"fehlt oder<br/>fremde org-id"| C["Kollisions-Fast-Path:<br/>GetObjectMetadata auf<br/>slug(name) (orgId)/"]
C -->|Gefunden| Z2[✓ Fertig\n2 Requests] C -->|"Marker vorhanden<br/>und org-id passt"| Z2["Fertig — 2 Requests"]
C -->|Nicht gefunden| D[Fallback: O-n\nListObjectsV2 alle\nTop-Level-Prefixes] C -->|"nicht gefunden"| D["Fallback O(n):<br/>alle Top-Level-Prefixes<br/>auflisten und über<br/>org-id filtern"]
D -->|Ordner mit\npAssender ID gefunden| E[Rename auf\nkanonischen Namen] D -->|"Ordner gefunden"| E["Rename auf<br/>kanonischen Namen"]
E --> Z3[✓ Fertig\nnächster Lookup O-1] E --> Z3["Fertig — nächster<br/>Lookup ist O(1)"]
D -->|Kein Ordner| F[Anlegen: Ordner +\nUnterstruktur 9560] D -->|"kein Ordner"| F["Kundenordner samt<br/>Typ-Unterordnern anlegen"]
F --> Z4[✓ Fertig\nSeite leer] F --> Z4["Fertig — Seite<br/>zeigt leeren Zustand"]
style Z1 fill:#d5e8d4,stroke:#82b366
style Z2 fill:#d5e8d4,stroke:#82b366
style Z3 fill:#fff2cc,stroke:#d6b656
style Z4 fill:#fff2cc,stroke:#d6b656
style D fill:#f8cecc,stroke:#b85450
Binary file not shown.
+39 -29
View File
@@ -1,29 +1,39 @@
classDiagram flowchart TB
class DocumentsPage { subgraph Pages["Pages"]
+OnGetAsync() P1["Documents.cshtml<br/>+ .cshtml.cs<br/><i>Liste, Suche, Filter, Seiten</i>"]
+OnGetDownloadAsync() P2["Document/Download.cshtml.cs<br/><i>Einzel- und ZIP-Download</i>"]
+OnGetZipAsync() P3["Document/Share.cshtml.cs<br/><i>OpenGraph-Vorschau</i>"]
} end
class DocumentsService {
+ListDocumentsAsync(orgId, search, type, token) subgraph Services["Services"]
+GetDownloadUrlAsync(orgId, key) S["DocumentsService<br/><i>Auflisten, Filtern, Aufloesen<br/>des Kundenordners</i>"]
+GetZipStreamAsync(orgId, keys) C["S3DocumentsClient<br/><i>Kapselung von IAmazonS3</i>"]
+ResolveCustomerPrefixAsync(orgId, orgName) end
}
class S3DocumentsClient { subgraph Model["Model/Documents"]
-IAmazonS3 _s3 M1["DocumentType<br/><i>Enum + Ableitung<br/>aus Ordnername</i>"]
+ListObjectsAsync(prefix, delimiter, token) M2["DocumentName<br/><i>Anzeigename,<br/>.url-Erkennung</i>"]
+GetObjectMetadataAsync(key) M3["DocumentKey /<br/>RelativePath<br/><i>validierte Pfade</i>"]
+GetPresignedUrlAsync(key) M4["OrgSlug<br/><i>Normalisierung des<br/>Firmennamens</i>"]
} M5["DocumentViewModel"]
class OrgSlug { end
+Slugify(name) string
} EXT["IAmazonS3<br/>(AWS SDK)"]
class S3Settings { CFG["S3Settings<br/><i>keyed service</i>"]
+BucketName string
+ServiceUrl string P1 --> S
} P2 --> S
DocumentsPage --> DocumentsService P3 --> S
DocumentsService --> S3DocumentsClient S --> C
DocumentsService --> OrgSlug S --> M1
S3DocumentsClient --> S3Settings S --> M2
S --> M4
S --> M5
P2 --> M3
C --> EXT
C --> CFG
style S fill:#dae8fc,stroke:#6c8ebf
style C fill:#dae8fc,stroke:#6c8ebf
style EXT fill:#f5f5f5,stroke:#999
style CFG fill:#f5f5f5,stroke:#999
Binary file not shown.
+18 -12
View File
@@ -1,15 +1,21 @@
sequenceDiagram sequenceDiagram
participant A as Request A autonumber
participant A as Anfrage A
participant S3 as S3-Speicher participant S3 as S3-Speicher
participant B as Request B participant B as Anfrage B
Note over A,B: Ordner AltName/ mit 200 Objekten, Zielname: Kunde GmbH/ Note over A,B: Ausgangslage: Ordner "AltName/" mit 200 Objekten,<br/>Zielname "Kunde GmbH/". Beide Anfragen gehoeren derselben Org.
A->>S3: CanClaim(Kunde GmbH) → Missing → true
B->>S3: CanClaim(Kunde GmbH) → Missing → true A->>S3: CanClaim("Kunde GmbH") → frei
A->>S3: Kopiere 200 Objekte nach Kunde GmbH/ B->>S3: CanClaim("Kunde GmbH") → frei
B->>S3: Kopiere Objekte #1–60 nach Kunde GmbH/ Note over A,B: Beide halten den Zielnamen fuer beanspruchbar
A->>S3: DeleteObjects(AltName/*) ✓
B->>S3: Kopie #61 → NoSuchKey ✗ A->>S3: kopiert alle 200 Objekte
B->>S3: catch: DeleteObjects(copiedKeys #1–60) B->>S3: kopiert Objekte 1 bis 60
Note over S3: 60 Objekte aus Kunde GmbH/ gelöscht! A->>S3: DeleteObjects("AltName/*")
Note over S3: AltName/ existiert nicht mehr → Datenverlust Note over S3: Quelle ist jetzt geloescht
B->>S3: kopiert Objekt 61 → NoSuchKey
B->>S3: catch: DeleteObjects(eigene 60 Kopien)
Note over S3: Die 60 geloeschten Objekte sind genau die,<br/>die A erfolgreich geschrieben hatte.<br/>Die Quelle existiert nicht mehr — Datenverlust.
Binary file not shown.
+20 -7
View File
@@ -1,9 +1,22 @@
flowchart TD flowchart TD
B[(S3-Bucket\ndocuments-houston-prod)] B["Bucket: documents-houston-prod"]
B --> KA["Kunde A/\n[meta: efecte_org-id=42]"] B --> KA["Beispielkunde GmbH/<br/><i>Marker-Objekt mit<br/>Metadatum efecte-org-id = 42</i>"]
B --> KB["Kunde B/\n[meta: efecte_org-id=77]"] B --> KB["Andere Firma AG/<br/><i>efecte-org-id = 77</i>"]
KA --> D1[doc1.pdf\nuntyped]
KA --> U1["Uebersicht.pdf<br/><i>ohne Typ</i>"]
KA --> T1["Service-Protokoll/"] KA --> T1["Service-Protokoll/"]
KA --> T2["Vertragsunterlagen/"] KA --> T2["Monitoring Reports/"]
T1 --> D2[bericht-2026-07.pdf] KA --> T3["Vertragsunterlagen/"]
T2 --> D3[SLA-2026.pdf] KA --> T4["... weitere Typordner"]
T1 --> D1["Protokoll-2026-07.pdf"]
T2 --> D2["Report-Juli.pdf"]
T3 --> D3["Rahmenvertrag.pdf"]
T3 --> D4["Sharepoint-Ablage.url"]
style KA fill:#dae8fc,stroke:#6c8ebf
style KB fill:#dae8fc,stroke:#6c8ebf
style T1 fill:#fff2cc,stroke:#d6b656
style T2 fill:#fff2cc,stroke:#d6b656
style T3 fill:#fff2cc,stroke:#d6b656
style T4 fill:#fff2cc,stroke:#d6b656
Binary file not shown.
Binary file not shown.
+15 -10
View File
@@ -1,10 +1,15 @@
%%{init: {'theme': 'base'}}%% %%{init: {'theme': 'base', 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 45}}}%%
timeline flowchart LR
title Infrastrukturbeschaffung S3-Speicher A["<b>22.07.2026</b><br/>Service Request<br/>im Maschinenraum"]
2026-07-22 : Service Request erstellt (Maschinenraum) B["<b>23.07.2026</b><br/>Zuweisung an<br/>Alexander Wagner"]
2026-07-23 : Request Alexander Wagner zugewiesen C["<b>27.07.2026</b><br/>Drei Buckets angelegt,<br/>Zugangsdaten in Passbolt"]
2026-07-27 : 3 Buckets durch AU erstellt, Credentials in Passbolt D["<b>30.07.2026</b><br/>Anfrage S3 Select und<br/>Search Integration"]
2026-07-30 : Anfrage S3 Select + Search Integration E["<b>04.08.2026</b><br/>Lennart Meinert<br/>kontaktiert AU"]
2026-08-04 : Lennart Meinert kontaktiert AU F["<b>07.08.2026</b><br/>S3 Select für alle<br/>drei Buckets aktiviert"]
2026-08-07 : S3 Select für alle 3 Buckets aktiviert G["<b>17.08.2026</b><br/>Search Integration<br/>von AU abgelehnt"]
2026-08-17 : Search Integration von AU abgelehnt
A --> B --> C --> D
E --> F --> G
classDef step fill:#eef2f7,stroke:#5b6b7f,stroke-width:1px,color:#111
class A,B,C,D,E,F,G step
Binary file not shown.
+16 -10
View File
@@ -1,15 +1,21 @@
sequenceDiagram sequenceDiagram
participant B as Browser autonumber
participant P as PageModel actor U as Benutzer
participant P as Download-PageModel
participant S as DocumentsService participant S as DocumentsService
participant Z as ZipArchive
participant S3 as S3-Speicher participant S3 as S3-Speicher
B->>P: POST /documents?handler=Zip&keys=[...] U->>P: Auswahl + Klick auf<br/>"Als ZIP herunterladen"
P->>S: GetZipStreamAsync(orgId, keys) P->>S: Schluessel gegen Kundenpraefix pruefen
loop je Dokument S-->>P: validierte Schluessel
S->>S3: GetObjectAsync(key) P->>U: Antwort-Header:<br/>application/zip
S3-->>S: Stream loop je ausgewaehltes Dokument
S->>S: ZipArchive.CreateEntry + CopyToAsync S->>S3: GetObject(key)
S3-->>S: Inhalts-Stream
S->>Z: Eintrag mit relativem Pfad anlegen
Z->>U: komprimierte Bytes<br/>direkt in die Antwort
end end
S-->>P: ZipStream Z->>U: Archiv abschliessen
P-->>B: application/zip (streaming)
Note over Z,U: Kein Zwischenspeichern im<br/>Arbeitsspeicher oder auf Platte
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
<svg width="21" height="28" viewBox="0 0 21 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.5 24C4.67157 24 4 23.3284 4 22.5V12.5C4 11.6716 4.67157 11 5.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761 13.7761 12 13.5 12H5.5C5.22386 12 5 12.2239 5 12.5V22.5C5 22.7761 5.22386 23 5.5 23H15.5C15.7761 23 16 22.7761 16 22.5V17.5C16 17.2239 16.2239 17 16.5 17C16.7761 17 17 17.2239 17 17.5V22.5C17 23.3284 16.3284 24 15.5 24H5.5Z" fill="black"/>
<path d="M10.8536 19.8536L17.8536 12.8536C18.0488 12.6583 18.0488 12.3417 17.8536 12.1464C17.6583 11.9512 17.3417 11.9512 17.1464 12.1464L10.5 18.7929L7.85355 16.1464C7.65829 15.9512 7.34171 15.9512 7.14645 16.1464C6.95118 16.3417 6.95118 16.6583 7.14645 16.8536L10.1464 19.8536C10.3417 20.0488 10.6583 20.0488 10.8536 19.8536Z" fill="black"/>
<path d="M0.5 24.0161C0.5 25.1774 1.1087 27.5 3.54348 27.5H17.4565C18.471 27.5 20.5 26.8032 20.5 24.0161V6.59677L14.413 0.5H3.54348C2.52899 0.5 0.5 1.10968 0.5 3.54839V24.0161Z" stroke="black"/>
<path d="M14.4131 4.71875V1.34375L19.6304 6.82812H16.587C15.8623 6.82812 14.4131 6.40625 14.4131 4.71875Z" fill="black" stroke="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
<svg width="21" height="28" viewBox="0 0 21 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.5 24.0161C0.5 25.1774 1.1087 27.5 3.54348 27.5H17.4565C18.471 27.5 20.5 26.8032 20.5 24.0161V6.59677L14.413 0.5H3.54348C2.52899 0.5 0.5 1.10968 0.5 3.54839V24.0161Z" stroke="black"/>
<path d="M14.4131 4.71875V1.34375L19.6304 6.82812H16.587C15.8623 6.82812 14.4131 6.40625 14.4131 4.71875Z" fill="black" stroke="black"/>
<path d="M10.5 21.4571H11.4962C11.8121 24.1767 13.6102 25.75 16.4449 25.75C17.0281 25.75 17.5383 25.6868 18 25.5761V24.3586C17.5464 24.4693 17.02 24.5167 16.4449 24.5167C14.4768 24.5167 13.2052 23.3941 12.9055 21.4571H16.6717V20.5875H12.8407C12.8407 20.5717 12.8407 20.5559 12.8407 20.5401V19.7099C12.8407 19.6072 12.8407 19.5044 12.8488 19.4016H16.6717V18.532H12.9541C13.3186 16.7532 14.5659 15.7333 16.4449 15.7333C17.02 15.7333 17.5464 15.7807 18 15.8993V14.6818C17.5383 14.5632 17.0281 14.5 16.4449 14.5C13.6992 14.5 11.9255 15.9705 11.5286 18.532H10.5V19.4016H11.4476C11.4476 19.4965 11.4476 19.5993 11.4476 19.6941V20.5875H10.5V21.4571Z" fill="black"/>
<line x1="18" y1="12" x2="3" y2="12" stroke="black" stroke-linecap="round"/>
<line x1="11" y1="15" x2="3" y2="15" stroke="black" stroke-linecap="round"/>
<line x1="9" y1="18" x2="3" y2="18" stroke="black" stroke-linecap="round"/>
<line x1="9" y1="21" x2="3" y2="21" stroke="black" stroke-linecap="round"/>
<line x1="10" y1="24" x2="3" y2="24" stroke="black" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
<svg width="21" height="28" viewBox="0 0 21 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.5 24.0161C0.5 25.1774 1.1087 27.5 3.54348 27.5H17.4565C18.471 27.5 20.5 26.8032 20.5 24.0161V6.59677L14.413 0.5H3.54348C2.52899 0.5 0.5 1.10968 0.5 3.54839V24.0161Z" stroke="black"/>
<path d="M14.4131 4.71875V1.34375L19.6304 6.82812H16.587C15.8623 6.82812 14.4131 6.40625 14.4131 4.71875Z" fill="black" stroke="black"/>
</svg>

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
<svg width="21" height="28" viewBox="0 0 21 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.5 10.5C8.71025 10.5 8.89804 10.6315 8.9699 10.8291L12.5 20.5369L14.0301 16.3291C14.102 16.1315 14.2897 16 14.5 16H18C18.2761 16 18.5 16.2239 18.5 16.5C18.5 16.7761 18.2761 17 18 17H14.8502L12.9699 22.1709C12.898 22.3685 12.7103 22.5 12.5 22.5C12.2897 22.5 12.102 22.3685 12.0301 22.1709L8.5 12.4631L6.9699 16.6709C6.89804 16.8685 6.71025 17 6.5 17H3C2.72386 17 2.5 16.7761 2.5 16.5C2.5 16.2239 2.72386 16 3 16H6.14979L8.0301 10.8291C8.10196 10.6315 8.28975 10.5 8.5 10.5Z" fill="black"/>
<path d="M0.5 24.0161C0.5 25.1774 1.1087 27.5 3.54348 27.5H17.4565C18.471 27.5 20.5 26.8032 20.5 24.0161V6.59677L14.413 0.5H3.54348C2.52899 0.5 0.5 1.10968 0.5 3.54839V24.0161Z" stroke="black"/>
<path d="M14.413 4.71875V1.34375L19.6304 6.82812H16.5869C15.8623 6.82812 14.413 6.40625 14.413 4.71875Z" fill="black" stroke="black"/>
</svg>

After

Width:  |  Height:  |  Size: 979 B

Binary file not shown.
+12
View File
@@ -0,0 +1,12 @@
<svg width="21" height="28" viewBox="0 0 21 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5826_132)">
<path d="M0.5 24.0161C0.5 25.1774 1.1087 27.5 3.54348 27.5H17.4565C18.471 27.5 20.5 26.8032 20.5 24.0161V6.59677L14.413 0.5H3.54348C2.52899 0.5 0.5 1.10968 0.5 3.54839V24.0161Z" stroke="black"/>
<path d="M14.4131 4.71875V1.34375L19.6304 6.82812H16.587C15.8623 6.82812 14.4131 6.40625 14.4131 4.71875Z" fill="black" stroke="black"/>
<path d="M7.78762 21.0079C6.85412 20.7746 6.33409 20.0328 6.33208 18.9311C6.33141 18.5793 6.35688 18.4104 6.44467 18.1839C6.65978 17.629 7.23074 17.2102 7.98062 17.056C8.35389 16.9797 8.46914 16.8972 8.46914 16.7062C8.46914 16.6466 8.51338 16.4683 8.56767 16.3102C8.81428 15.5931 9.27131 14.9947 9.75984 14.7501C10.2712 14.4941 10.5285 14.4365 11.147 14.4398C12.0249 14.4445 12.4632 14.6348 13.0757 15.2782L13.4128 15.632L13.7143 15.5275C15.1752 15.0222 16.6314 15.8826 16.7487 17.3208L16.7809 17.7141L17.0684 17.8173C17.89 18.1115 18.276 18.7294 18.2063 19.6381C18.1607 20.2325 17.8826 20.707 17.4423 20.9422L17.2352 21.0528L12.6374 21.0615C9.10444 21.0682 7.98196 21.0561 7.78762 21.0079ZM4.34043 20.3732C3.79561 20.2439 3.21795 19.7621 2.94721 19.2119C2.79375 18.8996 2.78571 18.8534 2.78571 18.3012C2.78571 17.7758 2.79911 17.6913 2.92107 17.4307C3.17908 16.8805 3.67297 16.4831 4.29285 16.3269C4.42353 16.2941 4.54683 16.2412 4.56627 16.2103C4.5857 16.1795 4.60714 16.0086 4.61452 15.831C4.6574 14.7307 5.37981 13.761 6.3877 13.4494C6.93253 13.2812 7.61674 13.3227 8.20914 13.5593C8.39678 13.6343 8.37601 13.6504 8.7734 13.1297C9.00862 12.8214 9.48308 12.4381 9.87176 12.2431C10.2913 12.0327 10.7275 11.9355 11.2489 11.9369C12.7071 11.9402 13.9636 12.8523 14.428 14.2441C14.5768 14.6891 14.5688 14.8131 14.3939 14.8171C14.3175 14.8184 14.0983 14.8607 13.9073 14.9103L13.5595 15.0007L13.2426 14.6838C12.3479 13.7891 10.8884 13.5961 9.64793 14.208C9.15203 14.4526 8.75396 14.803 8.45306 15.2601C8.23862 15.5858 7.96521 16.1942 7.96521 16.345C7.96521 16.4522 7.87877 16.5059 7.50818 16.6272C6.36157 17.0031 5.69277 17.8716 5.69277 18.9827C5.69277 19.3875 5.79731 19.882 5.94139 20.1655C5.99567 20.2727 6.0265 20.3739 6.00908 20.3913C5.96485 20.4356 4.53812 20.4201 4.34043 20.3732Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_5826_132">
<rect width="21" height="28" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

Some files were not shown because too many files have changed in this diff Show More