Compare commits

..
10 Commits
Author SHA1 Message Date
ChristophandClaude Opus 5.5 f62541ff13 AP0 Schritte 0.4 und 0.6: Antworten vom 05.10. und Claude Design 0.3 eingearbeitet
tests / ci (push) Canceled after 0s
- Fragenliste: erste Antworten von BauIn und ITM eingetragen, übrige Fragen auf „gestellt“
- Anmeldung mit eigenem Passwort, 2FA später Pflicht; keine E-Mails; Mehrfach-Upload ohne ZIP
- Nur Produktionsschlüssel: Tests rufen die echte API nie auf
- Stellungnahme BÜW und Österreich in Phase 2; „Vor dem Livegang zu klären“ in AP11
- Claude Design Stand 0.3 abgeglichen (AP13), Prüfmaske v4 in AP6
- Gitea von ITM als Push-Mirror eingetragen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:54:18 +02:00
ChristophandClaude Opus 5.5 2143f4e955 Detaillierter Projektplan mit Teilplänen und Fragenliste
- docs/projektplan.md neu gegliedert: Arbeitsweise mit Freigaben, Zuständigkeiten,
  Meilensteine mit Abnahmekriterien, Arbeitspakete mit Status, Zeitplan, Entscheidungen
- docs/plaene/: Teilplan je Arbeitspaket (detailliert für AP0, AP1, AP2, AP3, AP5, AP11,
  AP13; grob für AP4, AP6–AP10, AP12, Optionen) und Vorlage
- docs/fragen.md: alle offenen Fragen mit Status, Empfänger und betroffenem Teilplan
- CLAUDE.md: Umsetzung nur nach freigegebenem Teilplan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:47:05 +02:00
ChristophandClaude Opus 5.5 6aac8b0a16 Datenmodell Kern: Projekt, PFA, Vertrag, LV, Nachtrag, Dokument
- Mandant, Auftragnehmer, Projekt mit Suchreihenfolge, Abschnitt (PFA), Vertrag,
  Leistungsverzeichnis, Nachtrag (MKA nur als Bezug), Nachtragsposition, Dokument
- Projektmitglieder mit Projektrolle, Freigaben für vertrauliche Dokumente
- Mandant, Land und Projekt werden vom übergeordneten Datensatz übernommen;
  Fehlzuordnungen über Mandanten oder Projekte hinweg werden abgewiesen
- Feste Morph-Map, Factories, Demodaten (Musterbahn, PFA 1–4, N14/N16/N21), Tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:34:24 +02:00
ChristophandClaude Opus 5.5 ca09d23a78 Schlüssel-Proxy, Server-Anforderungen und gitleaks-Regel für API-Werk
- tools/ki-proxy: setzt API-Schlüssel aus der Windows-Anmeldeinformationsverwaltung ein,
  nur im Speicher, ohne Header-Protokoll; Tests gegen Platzhalter-Server
- docs/server-anforderungen.md für den ITM-Entwickler
- .gitleaks.toml erkennt zki_/zodl_/zocr_-Schlüssel
- CLAUDE.md, Plan, Tech-Stack: Schlüssel nie in .env, in der App verschlüsselt in der DB

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:25:30 +02:00
ChristophandClaude Opus 5.5 6d6f8f8f27 Projektplan: 30 Stunden pro Woche, Stand des Claude-Design-Prototyps
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:25:20 +02:00
ChristophandClaude Opus 5.5 d9cda4a7d3 Plan auf Team Entwickler + Claude Code und ITM-Entwickler umgestellt
- Aufwand in Personenwochen mit Claude Code (Kern ≈ 11 statt ≈ 23 PW)
- ITM-Entwickler: KI-API und Serveraufbau nach unseren Vorgaben
- Mehr Inhalt je Meilenstein, Optionen Matrix und Stellungnahme im Zeitplan
- CLAUDE.md: Arbeitsweise für Vibe Coding, neuer Umfang, Regeln zur KI-API

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:14:57 +02:00
ChristophandClaude Opus 5.5 56eb2ea455 Projektplan und Tech-Stack nach erster Besprechung mit BauIn angepasst
- Geprüft werden Nachträge je Position: "im Vertrag enthalten?" mit Fundstellen
- Gliederung Projekt → PFA → Vertrag → LVs, Suchreihenfolge über PFAs
- GAEB X86 als Grundlage, PDF zur Anzeige; LV-Suche als Meilenstein M2
- KI-API ITM über API-Werk: Sprachmodell vorhanden, Polling statt Webhook
- Bewertungsmatrix und Stellungnahme als Optionen; Höhe Phase 2, Regelwerk Phase 3
- Termine: 16.10. BauIn, M1 30.10., M2 20.11., M3 18.12., M4 29.01.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:00:29 +02:00
ChristophandClaude Opus 5.5 558e698e3b Server und Gitea bei ITM, Backups als offener Punkt
- ITM stellt Staging- und Produktionsserver sowie den Gitea für den
  Push-Mirror (Adresse folgt)
- Backups: Speicherort, Aufbewahrungsdauer und Zuständigkeit noch offen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 12:23:39 +02:00
ChristophandClaude Opus 5.5 30d80174ac Beteiligte festgehalten: KI-API ITM, Endkunde BauIn
- "KI-API des Kunden" heißt jetzt "KI-API ITM" (Tech-Stack, CLAUDE.md,
  Projektplan)
- ITM: Anbieter der KI-API, Projektleitung, abrechnende Firma;
  BauIn: Endkunde
- Zulieferungen im Projektplan nach ITM und BauIn getrennt; Hosting der
  VMs noch zu klären

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:33:23 +02:00
ChristophandClaude Opus 5.5 68033d7cec Projekt- und Zeitplan für das MVP ergänzt
Arbeitspakete mit Aufwand (≈ 24,5 PW), Kalenderplan KW 40/2026 bis
KW 6/2027 für zwei Entwickler, Meilensteine M1–M4, Zulieferungen des
Kunden, Risiken und Aufgabenliste (inkl. Meilisearch-Test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:41:23 +02:00
63 changed files with 4446 additions and 26 deletions
+4
View File
@@ -36,3 +36,7 @@ yarn-error.log
/boost.json
/opencode.json
/opencode.jsonc
# Python (tools/)
__pycache__/
*.pyc
+11
View File
@@ -0,0 +1,11 @@
# Eigene gitleaks-Regeln für KI-BauIN, zusätzlich zu den Standardregeln.
title = "KI-BauIN"
[extend]
useDefault = true
[[rules]]
id = "api-werk-schluessel"
description = "Schlüssel der KI-API von ITM (API-Werk): KI, OpenDataLoader, OCR"
regex = '''\bz(?:ki|odl|ocr)_[A-Za-z0-9_\-]{12,}'''
keywords = ["zki_", "zodl_", "zocr_"]
+40 -4
View File
@@ -1,7 +1,8 @@
# KI-BauIN – Hinweise für Claude Code
KI-gestützte Prüfung von Mehrkostenanzeigen (MKA) und Nachträgen im Bahnbau. Stack und
Begründungen: `docs/tech-stack.md`. Einrichtung und Adressen: `README.md`.
KI-gestützte Prüfung von Nachträgen im Bahnbau dem Grunde nach: Ist die Leistung einer
Nachtragsposition schon im Vertrag (LVs mit Vorbemerkungen) enthalten? Stack und Begründungen:
`docs/tech-stack.md`, Umfang und Plan: `docs/projektplan.md`, Einrichtung: `README.md`.
## Befehle (immer über Sail, PHP 8.5 im Container)
@@ -18,10 +19,33 @@ npm run build # Assets (oder npm run de
Vor jedem Commit: Tests, Pint und PHPStan ohne Fehler. Der gitleaks-Hook (`.githooks`)
wird nicht umgangen.
## Arbeitsweise
**Erst Plan, dann Umsetzung.** Umgesetzt wird nur, was in einem freigegebenen Teilplan unter
`docs/plaene/` steht (Status „freigegeben“ oder „in Arbeit“). Ablauf und Definition of Done stehen
in `docs/projektplan.md`, Abschnitt 1.
- Vor Beginn eines Arbeitspakets den Teilplan detaillieren und die Freigabe abwarten.
- Commits nennen den Schritt, z. B. „AP1 Schritt 1.7: …“, und der Teilplan wird abgehakt.
- Neue Erkenntnisse, Antworten und Wünsche zuerst in `docs/fragen.md` bzw. den Teilplan, dann in
den Code. Ideen außerhalb des Plans nicht umsetzen, sondern im Gesamtplan unter „Nach Phase 1“
notieren.
Das Projekt entsteht per Vibe Coding: Claude Code schreibt den Code, der Entwickler steuert,
testet im Browser und liest nicht jede Zeile. Deshalb:
- Jede Funktion kommt mit Tests; Tests werden nie abgeschwächt oder übersprungen, damit sie grün
werden.
- Für jede geschützte Aktion ein Test, der den Zugriff ohne Berechtigung prüft (auch Download,
Suchtreffer, Daten an das Sprachmodell).
- Kleine Schritte, je ein Commit mit verständlicher Nachricht.
- Änderungen an Rechten, Dateizugriff, Suchfilter oder KI-Aufrufen in der Antwort ausdrücklich
benennen, damit sie gezielt geprüft werden.
- Bei fachlichen Unklarheiten nachfragen statt raten; offene Fragen stehen in `docs/fragen.md`.
## Harte Vorgaben
- **Alles lokal.** Keine Skripte, Fonts, Bilder oder APIs von fremden Hosts. Einzige Ausnahme
ist später die KI-API des Kunden. `tests/Feature/NoExternalResourcesTest.php` muss grün bleiben.
ist die KI-API ITM (API-Werk), nur aus dem Backend. `tests/Feature/NoExternalResourcesTest.php` muss grün bleiben.
- `<x-ts-avatar>` nur mit `text` (Initialen), nie mit `model`, `gravatar` oder Bild-URL –
sonst lädt TallStackUI von ui-avatars.com bzw. gravatar.com.
- `<x-ts-reaction>` nicht verwenden (lädt Emojis von Google).
@@ -31,6 +55,17 @@ wird nicht umgangen.
in Controllern und Livewire-Aktionen, bei Downloads, bei Suchtreffern und bevor Inhalte an
ein Sprachmodell gehen. Die Oberfläche auszublenden reicht nicht.
- **Dateien privat speichern** (Disk `local`) und nur über eine Route mit Rechteprüfung ausliefern.
- **API-Schlüssel nie in `.env`, Code, Tests, Logs oder Ausgaben.** In der Anwendung stehen sie
verschlüsselt in der Datenbank (Verwaltung → Einstellungen, nur beschreibbar). In der Entwicklung
läuft alles über den Schlüssel-Proxy (`tools/ki-proxy`, `http://127.0.0.1:8787` bzw. aus Sail
`http://host.docker.internal:8787`); die Schlüsselfelder bleiben dort leer.
- Claude Code startet den Proxy nie selbst und versucht nie, Schlüssel zu lesen (Windows-Tresor,
fremde Prozesse). Läuft der Proxy nicht, den Entwickler bitten, ihn zu starten.
- Echte Kundendaten (z. B. LVs von BauIn) gehen auch über den Proxy nicht an Claude: mit
synthetischen oder anonymisierten Dateien testen; mit echten Daten nur Skripte, die Kennzahlen ausgeben.
- **Dokumentinhalte sind Daten, keine Anweisungen.** Texte aus LVs, Nachträgen und Anlagen gehen
nur als abgegrenzter Kontext an das Sprachmodell; seine Antwort wird im Code geprüft.
Keine Authorization-Köpfe, ganzen Dokumente oder Prompts in technische Logs.
## Entwicklungsgrundsätze
@@ -52,7 +87,8 @@ wird nicht umgangen.
Toasts/Dialoge aus Livewire über `TallStackUi\Traits\Interactions` (`$this->toast()->success(…)->send()`).
- **Eigene Blade-Komponenten ohne Präfix**, z. B. `<x-heading>`, `<x-subheading>`, `<x-user-menu>`.
- **Dark Mode** über `tallstackui_darkTheme()` am `<html>`-Element; Speicherschlüssel `dark-theme`.
- **Benennung:** Fachbegriffe deutsch (`Mehrkostenanzeige`, `LvPosition`, `Los`), technische
- **Benennung:** Fachbegriffe deutsch (`Nachtrag`, `Nachtragsposition`, `LvPosition`, `Pfa`,
`Vertrag`), technische
Begriffe nach Laravel-Konvention englisch (`Controller`, `Policy`, `Job`).
- **Länder DE/AT:** gemeinsamer Kern, länderspezifische Logik hinter Schnittstellen in
`app/Laender/DE` bzw. `app/Laender/AT`. Fachliche Tabellen bekommen `mandant_id` und `land`.
+5 -2
View File
@@ -46,6 +46,9 @@ Die Ports 80 und 3306 sind auf dem Entwicklungsrechner belegt, daher `APP_PORT`
## Festlegungen
- Keine Selbstregistrierung: Benutzer legt ein Administrator an.
- 2FA (Authenticator-App) und Passkeys sind verfügbar, 2FA ist noch nicht verpflichtend.
- 2FA (Authenticator-App) und Passkeys sind verfügbar. 2FA ist zunächst freiwillig und wird
später per Schalter Pflicht.
- Die Anwendung verschickt keine E-Mails.
- Meilisearch: Version fest eingestellt, Telemetrie aus (`MEILISEARCH_NO_ANALYTICS=true`).
- Git: führend ist der lokale Gitea; der Gitea des Kunden folgt als Push-Mirror.
- Git: führend ist der lokale Gitea; der Gitea von ITM
(`https://gitea.itm-technologies.de/ChristophGraf/BauIN`) folgt als Push-Mirror.
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Enums;
/** Art eines hochgeladenen Dokuments. */
enum DokumentKategorie: string
{
case LvX86 = 'lv_x86';
case LvD86 = 'lv_d86';
case LvPdf = 'lv_pdf';
case NachtragsLvX86 = 'nachtrags_lv_x86';
case NachtragsLvD86 = 'nachtrags_lv_d86';
case NachtragsLvPdf = 'nachtrags_lv_pdf';
case Anschreiben = 'anschreiben';
case Anlage = 'anlage';
case Vorlage = 'vorlage';
case Sonstiges = 'sonstiges';
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
/** Land mit eigenen Regeln (Normen, Begriffe, Anspruchsgrundlagen). */
enum Land: string
{
case DE = 'DE';
case AT = 'AT';
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Enums;
/** Bearbeitungsstand eines Nachtrags. */
enum NachtragStatus: string
{
case Eingegangen = 'eingegangen';
case Vorgeprueft = 'vorgeprueft';
case InPruefung = 'in_pruefung';
case Geprueft = 'geprueft';
case Exportiert = 'exportiert';
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace App\Enums;
/** Lebenszyklus eines Projekts; archivierte Projekte sind schreibgeschützt. */
enum ProjektStatus: string
{
case Aktiv = 'aktiv';
case Abgeschlossen = 'abgeschlossen';
case Archiviert = 'archiviert';
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace App\Enums;
/** Rolle eines Benutzers innerhalb eines Projekts. */
enum Projektrolle: string
{
case Projektleiter = 'projektleiter';
case Bearbeiter = 'bearbeiter';
case Leser = 'leser';
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace App\Enums;
/** Stand der Verarbeitung im Hintergrund (Import, Texterkennung, Indexierung). */
enum VerarbeitungStatus: string
{
case Wartet = 'wartet';
case Laeuft = 'laeuft';
case Fertig = 'fertig';
case Fehler = 'fehler';
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
/** Vertragsgrundlage eines Projekts. */
enum Vertragsgrundlage: string
{
case VobB = 'vob_b';
case Bgb = 'bgb';
}
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace App\Models;
use App\Enums\Land;
use App\Models\Concerns\ErbtZuordnung;
use Carbon\CarbonImmutable;
use Database\Factories\AbschnittFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Abschnitt eines Projekts, im Bahnbau meist ein PFA (Planfeststellungsabschnitt).
* Wie er in der Oberfläche heißt, steht am Projekt (abschnitt_bezeichnung).
*
* @property int $id
* @property int $mandant_id
* @property Land $land
* @property int $projekt_id
* @property string $bezeichnung
* @property int $sortierung
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
*/
#[Table('abschnitte')]
#[Fillable(['projekt_id', 'bezeichnung', 'sortierung'])]
class Abschnitt extends Model
{
/** @use HasFactory<AbschnittFactory> */
use ErbtZuordnung, HasFactory;
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'land' => Land::class,
];
}
protected function zuordnungsEltern(): ?Model
{
return $this->projekt()->first();
}
/** @return BelongsTo<Projekt, $this> */
public function projekt(): BelongsTo
{
return $this->belongsTo(Projekt::class);
}
/** @return HasMany<Vertrag, $this> */
public function vertraege(): HasMany
{
return $this->hasMany(Vertrag::class);
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace App\Models;
use App\Enums\Land;
use Carbon\CarbonImmutable;
use Database\Factories\AuftragnehmerFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Baufirma oder ARGE. Kann in mehreren Abschnitten eines Projekts Verträge haben.
*
* @property int $id
* @property int $mandant_id
* @property Land $land
* @property string $name
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
*/
#[Table('auftragnehmer')]
#[Fillable(['mandant_id', 'land', 'name'])]
class Auftragnehmer extends Model
{
/** @use HasFactory<AuftragnehmerFactory> */
use HasFactory;
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'land' => Land::class,
];
}
/** @return BelongsTo<Mandant, $this> */
public function mandant(): BelongsTo
{
return $this->belongsTo(Mandant::class);
}
/** @return HasMany<Vertrag, $this> */
public function vertraege(): HasMany
{
return $this->hasMany(Vertrag::class);
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace App\Models\Concerns;
use App\Models\Projekt;
use BackedEnum;
use Illuminate\Database\Eloquent\Model;
use LogicException;
/**
* Übernimmt mandant_id, land und projekt_id beim Speichern vom übergeordneten Datensatz.
*
* Zeigt ein Datensatz auf einen übergeordneten Datensatz eines anderen Mandanten, Landes oder
* Projekts, wird das Speichern abgewiesen. So kann kein Vertrag in Projekt A an einem Abschnitt
* aus Projekt B hängen.
*/
trait ErbtZuordnung
{
/**
* Der übergeordnete Datensatz, von dem die Zuordnung übernommen wird.
*/
abstract protected function zuordnungsEltern(): ?Model;
protected static function bootErbtZuordnung(): void
{
static::saving(function (self $model): void {
$model->uebernimmZuordnung();
});
}
protected function uebernimmZuordnung(): void
{
$eltern = $this->zuordnungsEltern();
if ($eltern === null) {
return;
}
$this->gleicheZuordnungAn($eltern, [
'mandant_id' => $eltern->getAttribute('mandant_id'),
'land' => $eltern->getAttribute('land'),
'projekt_id' => $eltern instanceof Projekt ? $eltern->getKey() : $eltern->getAttribute('projekt_id'),
]);
}
/**
* Setzt fehlende Werte und weist abweichende ab.
*
* @param array<string, mixed> $werte
*/
protected function gleicheZuordnungAn(Model $anderes, array $werte): void
{
foreach ($werte as $spalte => $wert) {
$eigen = $this->getAttribute($spalte);
if ($eigen === null) {
$this->setAttribute($spalte, $wert);
} elseif (self::vergleichswert($eigen) !== self::vergleichswert($wert)) {
throw new LogicException(sprintf(
'%s: %s passt nicht zu %s.',
class_basename($this),
$spalte,
class_basename($anderes),
));
}
}
}
private static function vergleichswert(mixed $wert): ?string
{
if ($wert instanceof BackedEnum) {
return (string) $wert->value;
}
return $wert === null ? null : (string) $wert;
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
namespace App\Models;
use App\Enums\DokumentKategorie;
use App\Enums\Land;
use App\Models\Concerns\ErbtZuordnung;
use Carbon\CarbonImmutable;
use Database\Factories\DokumentFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* Hochgeladene Datei im privaten Speicher. Gehört zu einem Projekt und optional zu einem LV,
* Nachtrag oder Vertrag. Ohne Projekt ist es ein allgemeines Dokument (Einstellungen).
* Vertrauliche Dokumente sehen nur die freigegebenen Personen.
*
* @property int $id
* @property int $mandant_id
* @property Land $land
* @property int|null $projekt_id
* @property string|null $dokumentable_type
* @property int|null $dokumentable_id
* @property DokumentKategorie $kategorie
* @property string $dateiname
* @property string $pfad
* @property string $mime_type
* @property int $groesse
* @property string $sha256
* @property bool $vertraulich
* @property int|null $hochgeladen_von
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
*/
#[Table('dokumente')]
#[Fillable([
'mandant_id', 'land', 'projekt_id', 'kategorie', 'dateiname', 'pfad', 'mime_type',
'groesse', 'sha256', 'vertraulich', 'hochgeladen_von',
])]
class Dokument extends Model
{
/** @use HasFactory<DokumentFactory> */
use ErbtZuordnung, HasFactory;
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'land' => Land::class,
'kategorie' => DokumentKategorie::class,
'vertraulich' => 'boolean',
'groesse' => 'integer',
];
}
protected function zuordnungsEltern(): ?Model
{
if ($this->dokumentable_type !== null) {
return $this->dokumentable()->getResults();
}
return $this->projekt()->first();
}
/** @return BelongsTo<Projekt, $this> */
public function projekt(): BelongsTo
{
return $this->belongsTo(Projekt::class);
}
/** @return MorphTo<Model, $this> */
public function dokumentable(): MorphTo
{
return $this->morphTo();
}
/** @return BelongsTo<User, $this> */
public function hochgeladenVon(): BelongsTo
{
return $this->belongsTo(User::class, 'hochgeladen_von');
}
/** @return BelongsToMany<User, $this> */
public function freigaben(): BelongsToMany
{
return $this->belongsToMany(User::class, 'dokument_freigaben')->withTimestamps();
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace App\Models;
use App\Enums\Land;
use App\Enums\VerarbeitungStatus;
use App\Models\Concerns\ErbtZuordnung;
use Carbon\CarbonImmutable;
use Database\Factories\LeistungsverzeichnisFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphMany;
/**
* Ein LV des Vertrags. Die Dateien (X86, D86, PDF) hängen als Dokumente daran,
* Positionen und Vorbemerkungen kommen mit dem GAEB-Import dazu.
*
* @property int $id
* @property int $mandant_id
* @property Land $land
* @property int $projekt_id
* @property int $vertrag_id
* @property string $bezeichnung
* @property VerarbeitungStatus $import_status
* @property string|null $import_fehler
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
*/
#[Table('leistungsverzeichnisse')]
#[Fillable(['vertrag_id', 'bezeichnung', 'import_status', 'import_fehler'])]
class Leistungsverzeichnis extends Model
{
/** @use HasFactory<LeistungsverzeichnisFactory> */
use ErbtZuordnung, HasFactory;
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'land' => Land::class,
'import_status' => VerarbeitungStatus::class,
];
}
protected function zuordnungsEltern(): ?Model
{
return $this->vertrag()->first();
}
/** @return BelongsTo<Projekt, $this> */
public function projekt(): BelongsTo
{
return $this->belongsTo(Projekt::class);
}
/** @return BelongsTo<Vertrag, $this> */
public function vertrag(): BelongsTo
{
return $this->belongsTo(Vertrag::class);
}
/** @return MorphMany<Dokument, $this> */
public function dokumente(): MorphMany
{
return $this->morphMany(Dokument::class, 'dokumentable');
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Carbon\CarbonImmutable;
use Database\Factories\MandantFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Kunde der Anwendung. Alle Fachdaten gehören genau einem Mandanten.
*
* @property int $id
* @property string $name
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
*/
#[Table('mandanten')]
#[Fillable(['name'])]
class Mandant extends Model
{
/** @use HasFactory<MandantFactory> */
use HasFactory;
/** @return HasMany<Projekt, $this> */
public function projekte(): HasMany
{
return $this->hasMany(Projekt::class);
}
/** @return HasMany<Auftragnehmer, $this> */
public function auftragnehmer(): HasMany
{
return $this->hasMany(Auftragnehmer::class);
}
/** @return HasMany<User, $this> */
public function benutzer(): HasMany
{
return $this->hasMany(User::class);
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
namespace App\Models;
use App\Enums\Land;
use App\Enums\NachtragStatus;
use App\Models\Concerns\ErbtZuordnung;
use Carbon\CarbonImmutable;
use Database\Factories\NachtragFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
/**
* Nachtrag eines Auftragnehmers zu einem Vertrag. Geprüft wird je Nachtragsposition, ob die
* Leistung schon im Vertrag enthalten ist. Die MKA ist nur eine Bezugsnummer (mka_bezug).
*
* @property int $id
* @property int $mandant_id
* @property Land $land
* @property int $projekt_id
* @property int $vertrag_id
* @property string $nummer
* @property CarbonImmutable $eingang_am
* @property string $betreff
* @property list<string>|null $mka_bezug
* @property NachtragStatus $status
* @property int|null $zugewiesen_an
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
*/
#[Table('nachtraege')]
#[Fillable(['vertrag_id', 'nummer', 'eingang_am', 'betreff', 'mka_bezug', 'status', 'zugewiesen_an'])]
class Nachtrag extends Model
{
/** @use HasFactory<NachtragFactory> */
use ErbtZuordnung, HasFactory;
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'land' => Land::class,
'eingang_am' => 'date',
'mka_bezug' => 'array',
'status' => NachtragStatus::class,
];
}
protected function zuordnungsEltern(): ?Model
{
return $this->vertrag()->first();
}
/** @return BelongsTo<Projekt, $this> */
public function projekt(): BelongsTo
{
return $this->belongsTo(Projekt::class);
}
/** @return BelongsTo<Vertrag, $this> */
public function vertrag(): BelongsTo
{
return $this->belongsTo(Vertrag::class);
}
/** @return BelongsTo<User, $this> */
public function zugewiesenAn(): BelongsTo
{
return $this->belongsTo(User::class, 'zugewiesen_an');
}
/** @return HasMany<Nachtragsposition, $this> */
public function positionen(): HasMany
{
return $this->hasMany(Nachtragsposition::class)->orderBy('sortierung')->orderBy('id');
}
/** @return MorphMany<Dokument, $this> */
public function dokumente(): MorphMany
{
return $this->morphMany(Dokument::class, 'dokumentable');
}
}
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace App\Models;
use App\Enums\Land;
use App\Models\Concerns\ErbtZuordnung;
use Carbon\CarbonImmutable;
use Database\Factories\NachtragspositionFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Position eines Nachtrags-LV. Der Einheitspreis ist in Phase 1 nur Information.
*
* @property int $id
* @property int $mandant_id
* @property Land $land
* @property int $projekt_id
* @property int $nachtrag_id
* @property string $oz
* @property string $kurztext
* @property string|null $langtext
* @property string|null $menge
* @property string|null $einheit
* @property string|null $einheitspreis
* @property int $sortierung
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
*/
#[Table('nachtragspositionen')]
#[Fillable(['nachtrag_id', 'oz', 'kurztext', 'langtext', 'menge', 'einheit', 'einheitspreis', 'sortierung'])]
class Nachtragsposition extends Model
{
/** @use HasFactory<NachtragspositionFactory> */
use ErbtZuordnung, HasFactory;
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'land' => Land::class,
'menge' => 'decimal:3',
'einheitspreis' => 'decimal:2',
];
}
protected function zuordnungsEltern(): ?Model
{
return $this->nachtrag()->first();
}
/** @return BelongsTo<Nachtrag, $this> */
public function nachtrag(): BelongsTo
{
return $this->belongsTo(Nachtrag::class);
}
}
+116
View File
@@ -0,0 +1,116 @@
<?php
namespace App\Models;
use App\Enums\Land;
use App\Enums\Projektrolle;
use App\Enums\ProjektStatus;
use App\Enums\Vertragsgrundlage;
use Carbon\CarbonImmutable;
use Database\Factories\ProjektFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Bauprojekt, z. B. „Elektrifizierung Musterbahn“. Gliedert sich in Abschnitte (PFA).
*
* @property int $id
* @property int $mandant_id
* @property Land $land
* @property string $name
* @property string|null $nummer
* @property string $auftraggeber
* @property Vertragsgrundlage $vertragsgrundlage
* @property string $abschnitt_bezeichnung
* @property ProjektStatus $status
* @property bool $suche_andere_vertraege_im_abschnitt
* @property bool $suche_andere_abschnitte
* @property bool $suche_nur_gleicher_auftragnehmer
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
*/
#[Table('projekte')]
#[Fillable([
'mandant_id', 'land', 'name', 'nummer', 'auftraggeber', 'vertragsgrundlage',
'abschnitt_bezeichnung', 'status', 'suche_andere_vertraege_im_abschnitt',
'suche_andere_abschnitte', 'suche_nur_gleicher_auftragnehmer',
])]
class Projekt extends Model
{
/** @use HasFactory<ProjektFactory> */
use HasFactory;
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'land' => Land::class,
'vertragsgrundlage' => Vertragsgrundlage::class,
'status' => ProjektStatus::class,
'suche_andere_vertraege_im_abschnitt' => 'boolean',
'suche_andere_abschnitte' => 'boolean',
'suche_nur_gleicher_auftragnehmer' => 'boolean',
];
}
/** @return BelongsTo<Mandant, $this> */
public function mandant(): BelongsTo
{
return $this->belongsTo(Mandant::class);
}
/** @return HasMany<Abschnitt, $this> */
public function abschnitte(): HasMany
{
return $this->hasMany(Abschnitt::class)->orderBy('sortierung')->orderBy('id');
}
/** @return HasMany<Vertrag, $this> */
public function vertraege(): HasMany
{
return $this->hasMany(Vertrag::class);
}
/** @return HasMany<Nachtrag, $this> */
public function nachtraege(): HasMany
{
return $this->hasMany(Nachtrag::class);
}
/** @return HasMany<Dokument, $this> */
public function dokumente(): HasMany
{
return $this->hasMany(Dokument::class);
}
/** @return BelongsToMany<User, $this> */
public function mitglieder(): BelongsToMany
{
return $this->belongsToMany(User::class, 'projekt_mitglieder')
->withPivot('rolle')
->withTimestamps();
}
/**
* Rolle des Benutzers in diesem Projekt oder null, wenn er kein Mitglied ist.
*/
public function rolleVon(User $user): ?Projektrolle
{
/** @var string|null $rolle */
$rolle = $this->mitglieder()->whereKey($user->getKey())->value('projekt_mitglieder.rolle');
return $rolle === null ? null : Projektrolle::from($rolle);
}
public function istArchiviert(): bool
{
return $this->status === ProjektStatus::Archiviert;
}
}
+21
View File
@@ -7,6 +7,8 @@ use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
@@ -18,6 +20,7 @@ use Spatie\Permission\Traits\HasRoles;
/**
* @property int $id
* @property int|null $mandant_id
* @property string $name
* @property string $email
* @property string|null $locale
@@ -50,6 +53,24 @@ class User extends Authenticatable implements MustVerifyEmail, PasskeyUser
];
}
/** @return BelongsTo<Mandant, $this> */
public function mandant(): BelongsTo
{
return $this->belongsTo(Mandant::class);
}
/**
* Projekte, in denen der Benutzer Mitglied ist; die Projektrolle steht im Pivot (rolle).
*
* @return BelongsToMany<Projekt, $this>
*/
public function projekte(): BelongsToMany
{
return $this->belongsToMany(Projekt::class, 'projekt_mitglieder')
->withPivot('rolle')
->withTimestamps();
}
/**
* Get the user's initials
*/
+105
View File
@@ -0,0 +1,105 @@
<?php
namespace App\Models;
use App\Enums\Land;
use App\Models\Concerns\ErbtZuordnung;
use Carbon\CarbonImmutable;
use Database\Factories\VertragFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
/**
* Auftrag an einen Auftragnehmer innerhalb eines Abschnitts, z. B. „Gleisbau“.
* Sein Bau-Soll steht in den zugehörigen Leistungsverzeichnissen.
*
* @property int $id
* @property int $mandant_id
* @property Land $land
* @property int $projekt_id
* @property int $abschnitt_id
* @property int $auftragnehmer_id
* @property string $bezeichnung
* @property string|null $vertragsnummer
* @property CarbonImmutable|null $vertrag_vom
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
*/
#[Table('vertraege')]
#[Fillable(['abschnitt_id', 'auftragnehmer_id', 'bezeichnung', 'vertragsnummer', 'vertrag_vom'])]
class Vertrag extends Model
{
/** @use HasFactory<VertragFactory> */
use ErbtZuordnung, HasFactory;
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'land' => Land::class,
'vertrag_vom' => 'date',
];
}
protected static function booted(): void
{
static::saving(function (Vertrag $vertrag): void {
$auftragnehmer = $vertrag->auftragnehmer()->first();
if ($auftragnehmer !== null) {
$vertrag->gleicheZuordnungAn($auftragnehmer, [
'mandant_id' => $auftragnehmer->mandant_id,
'land' => $auftragnehmer->land,
]);
}
});
}
protected function zuordnungsEltern(): ?Model
{
return $this->abschnitt()->first();
}
/** @return BelongsTo<Projekt, $this> */
public function projekt(): BelongsTo
{
return $this->belongsTo(Projekt::class);
}
/** @return BelongsTo<Abschnitt, $this> */
public function abschnitt(): BelongsTo
{
return $this->belongsTo(Abschnitt::class);
}
/** @return BelongsTo<Auftragnehmer, $this> */
public function auftragnehmer(): BelongsTo
{
return $this->belongsTo(Auftragnehmer::class);
}
/** @return HasMany<Leistungsverzeichnis, $this> */
public function leistungsverzeichnisse(): HasMany
{
return $this->hasMany(Leistungsverzeichnis::class);
}
/** @return HasMany<Nachtrag, $this> */
public function nachtraege(): HasMany
{
return $this->hasMany(Nachtrag::class);
}
/** @return MorphMany<Dokument, $this> */
public function dokumente(): MorphMany
{
return $this->morphMany(Dokument::class, 'dokumentable');
}
}
+15
View File
@@ -2,7 +2,13 @@
namespace App\Providers;
use App\Models\Leistungsverzeichnis;
use App\Models\Nachtrag;
use App\Models\Projekt;
use App\Models\User;
use App\Models\Vertrag;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
@@ -33,6 +39,15 @@ class AppServiceProvider extends ServiceProvider
{
Date::use(CarbonImmutable::class);
// Feste Namen für polymorphe Beziehungen statt Klassennamen in der Datenbank.
Relation::enforceMorphMap([
'user' => User::class,
'projekt' => Projekt::class,
'vertrag' => Vertrag::class,
'leistungsverzeichnis' => Leistungsverzeichnis::class,
'nachtrag' => Nachtrag::class,
]);
DB::prohibitDestructiveCommands(
app()->isProduction(),
);
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace Database\Factories;
use App\Models\Abschnitt;
use App\Models\Projekt;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Abschnitt>
*/
class AbschnittFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'projekt_id' => Projekt::factory(),
'bezeichnung' => 'PFA '.fake()->unique()->numberBetween(1, 999999),
'sortierung' => 0,
];
}
}
@@ -0,0 +1,26 @@
<?php
namespace Database\Factories;
use App\Enums\Land;
use App\Models\Auftragnehmer;
use App\Models\Mandant;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Auftragnehmer>
*/
class AuftragnehmerFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'mandant_id' => Mandant::factory(),
'land' => Land::DE,
'name' => fake()->unique()->company(),
];
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace Database\Factories;
use App\Enums\DokumentKategorie;
use App\Models\Dokument;
use App\Models\Projekt;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
/**
* @extends Factory<Dokument>
*/
class DokumentFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
$uuid = (string) Str::uuid();
return [
'projekt_id' => Projekt::factory(),
'kategorie' => DokumentKategorie::Sonstiges,
'dateiname' => 'beispiel.pdf',
'pfad' => 'dokumente/'.$uuid.'.pdf',
'mime_type' => 'application/pdf',
'groesse' => fake()->numberBetween(10_000, 5_000_000),
'sha256' => hash('sha256', $uuid),
'vertraulich' => false,
];
}
/**
* Dokument zu einem LV, Nachtrag oder Vertrag; Projekt, Mandant und Land kommen von dort.
*/
public function zu(Model $dokumentable, DokumentKategorie $kategorie): static
{
return $this->state(fn (array $attributes) => [
'projekt_id' => null,
'dokumentable_type' => $dokumentable->getMorphClass(),
'dokumentable_id' => $dokumentable->getKey(),
'kategorie' => $kategorie,
]);
}
public function vertraulich(): static
{
return $this->state(fn (array $attributes) => ['vertraulich' => true]);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Database\Factories;
use App\Enums\VerarbeitungStatus;
use App\Models\Leistungsverzeichnis;
use App\Models\Vertrag;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Leistungsverzeichnis>
*/
class LeistungsverzeichnisFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'vertrag_id' => Vertrag::factory(),
'bezeichnung' => sprintf('LV %02d %s', fake()->numberBetween(1, 35), fake()->randomElement([
'Baustelleneinrichtung', 'Oberbau', 'Entwässerung', 'Kabeltiefbau', 'Erdbau',
])),
'import_status' => VerarbeitungStatus::Wartet,
];
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace Database\Factories;
use App\Models\Mandant;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Mandant>
*/
class MandantFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->unique()->company(),
];
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Database\Factories;
use App\Enums\NachtragStatus;
use App\Models\Nachtrag;
use App\Models\Vertrag;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Nachtrag>
*/
class NachtragFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'vertrag_id' => Vertrag::factory(),
'nummer' => 'N'.fake()->unique()->numberBetween(1, 999999),
'eingang_am' => fake()->dateTimeBetween('-3 months'),
'betreff' => fake()->sentence(4),
'mka_bezug' => ['MKA '.fake()->numberBetween(1, 200)],
'status' => NachtragStatus::Eingegangen,
];
}
}
@@ -0,0 +1,30 @@
<?php
namespace Database\Factories;
use App\Models\Nachtrag;
use App\Models\Nachtragsposition;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Nachtragsposition>
*/
class NachtragspositionFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'nachtrag_id' => Nachtrag::factory(),
'oz' => sprintf('01.%07d', fake()->unique()->numberBetween(1, 9999999)),
'kurztext' => fake()->sentence(5),
'langtext' => fake()->paragraph(),
'menge' => fake()->randomFloat(3, 1, 2000),
'einheit' => fake()->randomElement(['m', 'm²', 'm³', 'St', 'psch']),
'einheitspreis' => fake()->randomFloat(2, 1, 500),
'sortierung' => 0,
];
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Database\Factories;
use App\Enums\Land;
use App\Enums\ProjektStatus;
use App\Enums\Vertragsgrundlage;
use App\Models\Mandant;
use App\Models\Projekt;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Projekt>
*/
class ProjektFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'mandant_id' => Mandant::factory(),
'land' => Land::DE,
'name' => 'Elektrifizierung '.fake()->city(),
'nummer' => fake()->numerify('P-####'),
'auftraggeber' => 'DB InfraGO AG',
'vertragsgrundlage' => Vertragsgrundlage::VobB,
'abschnitt_bezeichnung' => 'PFA',
'status' => ProjektStatus::Aktiv,
];
}
public function archiviert(): static
{
return $this->state(fn (array $attributes) => ['status' => ProjektStatus::Archiviert]);
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace Database\Factories;
use App\Models\Abschnitt;
use App\Models\Auftragnehmer;
use App\Models\Vertrag;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Vertrag>
*/
class VertragFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'abschnitt_id' => Abschnitt::factory(),
// Auftragnehmer im selben Mandanten und Land wie der Abschnitt
'auftragnehmer_id' => function (array $attributes) {
$abschnitt = Abschnitt::query()->whereKey($attributes['abschnitt_id'])->firstOrFail();
return Auftragnehmer::factory()->state([
'mandant_id' => $abschnitt->mandant_id,
'land' => $abschnitt->land,
]);
},
'bezeichnung' => fake()->randomElement(['Gleisbau', 'Tunnelbau', 'Oberleitung', 'Kabeltiefbau']),
'vertragsnummer' => fake()->numerify('V-#####'),
'vertrag_vom' => fake()->dateTimeBetween('-2 years', '-1 month'),
];
}
}
@@ -0,0 +1,183 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Kern des Datenmodells: Mandant → Projekt → Abschnitt (PFA) → Vertrag → LVs und Nachträge.
* Beschreibung in docs/datenmodell.md.
*/
public function up(): void
{
Schema::create('mandanten', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->timestamps();
});
Schema::table('users', function (Blueprint $table) {
$table->foreignId('mandant_id')->nullable()->after('id')->constrained('mandanten')->restrictOnDelete();
});
Schema::create('auftragnehmer', function (Blueprint $table) {
$table->id();
$table->foreignId('mandant_id')->constrained('mandanten')->restrictOnDelete();
$table->char('land', 2);
$table->string('name');
$table->timestamps();
$table->unique(['mandant_id', 'land', 'name']);
});
Schema::create('projekte', function (Blueprint $table) {
$table->id();
$table->foreignId('mandant_id')->constrained('mandanten')->restrictOnDelete();
$table->char('land', 2);
$table->string('name');
$table->string('nummer', 100)->nullable();
$table->string('auftraggeber');
$table->string('vertragsgrundlage', 20)->default('vob_b');
$table->string('abschnitt_bezeichnung', 50)->default('PFA');
$table->string('status', 20)->default('aktiv');
$table->boolean('suche_andere_vertraege_im_abschnitt')->default(false);
$table->boolean('suche_andere_abschnitte')->default(true);
$table->boolean('suche_nur_gleicher_auftragnehmer')->default(false);
$table->timestamps();
$table->index(['mandant_id', 'land', 'status']);
});
Schema::create('projekt_mitglieder', function (Blueprint $table) {
$table->foreignId('projekt_id')->constrained('projekte')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->string('rolle', 20);
$table->timestamps();
$table->primary(['projekt_id', 'user_id']);
});
Schema::create('abschnitte', function (Blueprint $table) {
$table->id();
$table->foreignId('mandant_id')->constrained('mandanten')->restrictOnDelete();
$table->char('land', 2);
$table->foreignId('projekt_id')->constrained('projekte')->restrictOnDelete();
$table->string('bezeichnung', 100);
$table->unsignedSmallInteger('sortierung')->default(0);
$table->timestamps();
$table->unique(['projekt_id', 'bezeichnung']);
});
Schema::create('vertraege', function (Blueprint $table) {
$table->id();
$table->foreignId('mandant_id')->constrained('mandanten')->restrictOnDelete();
$table->char('land', 2);
$table->foreignId('projekt_id')->constrained('projekte')->restrictOnDelete();
$table->foreignId('abschnitt_id')->constrained('abschnitte')->restrictOnDelete();
$table->foreignId('auftragnehmer_id')->constrained('auftragnehmer')->restrictOnDelete();
$table->string('bezeichnung', 150);
$table->string('vertragsnummer', 100)->nullable();
$table->date('vertrag_vom')->nullable();
$table->timestamps();
});
Schema::create('leistungsverzeichnisse', function (Blueprint $table) {
$table->id();
$table->foreignId('mandant_id')->constrained('mandanten')->restrictOnDelete();
$table->char('land', 2);
$table->foreignId('projekt_id')->constrained('projekte')->restrictOnDelete();
$table->foreignId('vertrag_id')->constrained('vertraege')->restrictOnDelete();
$table->string('bezeichnung');
$table->string('import_status', 20)->default('wartet');
$table->text('import_fehler')->nullable();
$table->timestamps();
});
Schema::create('nachtraege', function (Blueprint $table) {
$table->id();
$table->foreignId('mandant_id')->constrained('mandanten')->restrictOnDelete();
$table->char('land', 2);
$table->foreignId('projekt_id')->constrained('projekte')->restrictOnDelete();
$table->foreignId('vertrag_id')->constrained('vertraege')->restrictOnDelete();
$table->string('nummer', 50);
$table->date('eingang_am');
$table->string('betreff');
$table->json('mka_bezug')->nullable();
$table->string('status', 20)->default('eingegangen');
$table->foreignId('zugewiesen_an')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->unique(['vertrag_id', 'nummer']);
$table->index(['projekt_id', 'status']);
});
Schema::create('nachtragspositionen', function (Blueprint $table) {
$table->id();
$table->foreignId('mandant_id')->constrained('mandanten')->restrictOnDelete();
$table->char('land', 2);
$table->foreignId('projekt_id')->constrained('projekte')->restrictOnDelete();
$table->foreignId('nachtrag_id')->constrained('nachtraege')->cascadeOnDelete();
$table->string('oz', 50);
$table->string('kurztext', 500);
$table->text('langtext')->nullable();
$table->decimal('menge', 15, 3)->nullable();
$table->string('einheit', 20)->nullable();
$table->decimal('einheitspreis', 15, 2)->nullable();
$table->unsignedInteger('sortierung')->default(0);
$table->timestamps();
$table->unique(['nachtrag_id', 'oz']);
});
Schema::create('dokumente', function (Blueprint $table) {
$table->id();
$table->foreignId('mandant_id')->constrained('mandanten')->restrictOnDelete();
$table->char('land', 2);
$table->foreignId('projekt_id')->nullable()->constrained('projekte')->restrictOnDelete();
$table->nullableMorphs('dokumentable');
$table->string('kategorie', 40);
$table->string('dateiname');
$table->string('pfad', 500);
$table->string('mime_type', 150);
$table->unsignedBigInteger('groesse');
$table->char('sha256', 64);
$table->boolean('vertraulich')->default(false);
$table->foreignId('hochgeladen_von')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->index(['projekt_id', 'kategorie']);
$table->index('sha256');
});
Schema::create('dokument_freigaben', function (Blueprint $table) {
$table->foreignId('dokument_id')->constrained('dokumente')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->timestamps();
$table->primary(['dokument_id', 'user_id']);
});
}
public function down(): void
{
Schema::dropIfExists('dokument_freigaben');
Schema::dropIfExists('dokumente');
Schema::dropIfExists('nachtragspositionen');
Schema::dropIfExists('nachtraege');
Schema::dropIfExists('leistungsverzeichnisse');
Schema::dropIfExists('vertraege');
Schema::dropIfExists('abschnitte');
Schema::dropIfExists('projekt_mitglieder');
Schema::dropIfExists('projekte');
Schema::dropIfExists('auftragnehmer');
Schema::table('users', function (Blueprint $table) {
$table->dropConstrainedForeignId('mandant_id');
});
Schema::dropIfExists('mandanten');
}
};
+4
View File
@@ -21,5 +21,9 @@ class DatabaseSeeder extends Seeder
'name' => 'Test User',
'email' => 'test@example.com',
]);
if (app()->isLocal()) {
$this->call(DemoSeeder::class);
}
}
}
+127
View File
@@ -0,0 +1,127 @@
<?php
namespace Database\Seeders;
use App\Enums\Land;
use App\Enums\NachtragStatus;
use App\Enums\Projektrolle;
use App\Enums\VerarbeitungStatus;
use App\Models\Abschnitt;
use App\Models\Auftragnehmer;
use App\Models\Mandant;
use App\Models\Nachtrag;
use App\Models\Projekt;
use App\Models\User;
use App\Models\Vertrag;
use Illuminate\Database\Seeder;
/**
* Erfundene Beispieldaten für Entwicklung und Vorführung (dieselben wie in der
* Rückmeldung an Claude Design). Keine echten Kundendaten.
*/
class DemoSeeder extends Seeder
{
public const PROJEKT = 'Elektrifizierung Musterbahn';
public function run(): void
{
if (Projekt::query()->where('name', self::PROJEKT)->exists()) {
$this->command->info('Demodaten sind schon vorhanden.');
return;
}
$mandant = Mandant::query()->firstOrCreate(['name' => 'Demo-Mandant']);
$projekt = Projekt::query()->create([
'mandant_id' => $mandant->id,
'land' => Land::DE,
'name' => self::PROJEKT,
'nummer' => 'P-2025-01',
'auftraggeber' => 'DB InfraGO AG',
]);
$an = fn (string $name) => Auftragnehmer::query()->firstOrCreate(
['mandant_id' => $mandant->id, 'land' => Land::DE, 'name' => $name],
);
$arge = $an('ARGE Musterbau');
$tunnel = $an('Tunnelbau Beispiel GmbH');
$oberleitung = $an('Oberleitung Nord GmbH');
$abschnitte = [];
foreach ([1, 2, 3, 4] as $nr) {
$abschnitte[$nr] = Abschnitt::query()->create([
'projekt_id' => $projekt->id,
'bezeichnung' => 'PFA '.$nr,
'sortierung' => $nr,
]);
}
$vertrag = fn (Abschnitt $abschnitt, Auftragnehmer $auftragnehmer, string $bezeichnung) => Vertrag::query()->create([
'abschnitt_id' => $abschnitt->id,
'auftragnehmer_id' => $auftragnehmer->id,
'bezeichnung' => $bezeichnung,
'vertrag_vom' => '2025-03-01',
]);
$vertrag($abschnitte[1], $oberleitung, 'Oberleitung');
$vertrag($abschnitte[2], $oberleitung, 'Oberleitung');
$gleisbauPfa3 = $vertrag($abschnitte[3], $arge, 'Gleisbau');
$vertrag($abschnitte[3], $tunnel, 'Tunnelbau');
$vertrag($abschnitte[3], $oberleitung, 'Oberleitung');
$gleisbauPfa4 = $vertrag($abschnitte[4], $arge, 'Gleisbau');
$vertrag($abschnitte[4], $tunnel, 'Tunnelbau');
$vertrag($abschnitte[4], $oberleitung, 'Oberleitung');
foreach (['LV 01 Baustelleneinrichtung', 'LV 03 Oberbau', 'LV 05 Entwässerung', 'LV 07 Kabeltiefbau'] as $bezeichnung) {
$gleisbauPfa4->leistungsverzeichnisse()->create(['bezeichnung' => $bezeichnung, 'import_status' => VerarbeitungStatus::Wartet]);
$gleisbauPfa3->leistungsverzeichnisse()->create(['bezeichnung' => $bezeichnung, 'import_status' => VerarbeitungStatus::Wartet]);
}
$this->nachtrag($gleisbauPfa4, 'N14', 'Lieferung besohlter Schwellen', '2026-09-29', ['MKA 112'], [
['N14.0010', 'Schwelle B70 mit Besohlung liefern', 420, 'St', 137.50],
['N14.0020', 'Zulage Verlegen besohlter Schwellen', 420, 'St', 12.40],
]);
$this->nachtrag($gleisbauPfa4, 'N16', 'Kamerabefahrung und Spülen der Entwässerungsleitungen', '2026-09-15', ['MKA 98'], [
['N16.0010', 'Kamerabefahrung Leitungen DN 160', 850, 'm', 3.20],
['N16.0020', 'Leitungen DN 160 spülen', 850, 'm', 2.10],
]);
$this->nachtrag($gleisbauPfa4, 'N21', 'Kabelschutzrohr DN 110 verlegen', '2026-10-01', ['MKA 131', 'MKA 133'], [
['N21.0010', 'Kabelschutzrohr DN 110 verlegen', 1200, 'm', 36.80],
]);
$testbenutzer = User::query()->where('email', 'test@example.com')->first();
if ($testbenutzer !== null) {
$testbenutzer->mandant()->associate($mandant)->save();
$projekt->mitglieder()->attach($testbenutzer, ['rolle' => Projektrolle::Projektleiter->value]);
}
}
/**
* @param list<string> $mka
* @param list<array{0: string, 1: string, 2: int|float, 3: string, 4: float}> $positionen
*/
private function nachtrag(Vertrag $vertrag, string $nummer, string $betreff, string $eingang, array $mka, array $positionen): Nachtrag
{
$nachtrag = $vertrag->nachtraege()->create([
'nummer' => $nummer,
'betreff' => $betreff,
'eingang_am' => $eingang,
'mka_bezug' => $mka,
'status' => NachtragStatus::Eingegangen,
]);
foreach ($positionen as $i => [$oz, $kurztext, $menge, $einheit, $ep]) {
$nachtrag->positionen()->create([
'oz' => $oz,
'kurztext' => $kurztext,
'menge' => $menge,
'einheit' => $einheit,
'einheitspreis' => $ep,
'sortierung' => $i,
]);
}
return $nachtrag;
}
}
+62
View File
@@ -0,0 +1,62 @@
# KI-BauIN – Datenmodell (Kern)
Stand: 5. Oktober 2026. Grundlage für Phase 1. Später ergänzt werden LV-Positionen und
Vorbemerkungen (GAEB-Import, AP5), Prüfung, Feedback und Regeln (AP6–AP8) sowie die Einstellungen
der KI-Schnittstelle (AP3).
## Übersicht
```mermaid
erDiagram
MANDANT ||--o{ PROJEKT : hat
MANDANT ||--o{ AUFTRAGNEHMER : hat
MANDANT ||--o{ USER : hat
PROJEKT ||--o{ ABSCHNITT : "gliedert sich in (PFA)"
ABSCHNITT ||--o{ VERTRAG : enthält
AUFTRAGNEHMER ||--o{ VERTRAG : "ist Partner von"
VERTRAG ||--o{ LEISTUNGSVERZEICHNIS : "hat (oft 20–35)"
VERTRAG ||--o{ NACHTRAG : "erhält"
NACHTRAG ||--o{ NACHTRAGSPOSITION : hat
PROJEKT ||--o{ DOKUMENT : "enthält"
LEISTUNGSVERZEICHNIS ||--o{ DOKUMENT : "X86, D86, PDF"
NACHTRAG ||--o{ DOKUMENT : "Nachtrags-LV, Anschreiben, Anlagen"
PROJEKT }o--o{ USER : "Mitglieder mit Projektrolle"
DOKUMENT }o--o{ USER : "Freigaben (vertraulich)"
```
| Modell | Tabelle | Inhalt |
|---|---|---|
| `Mandant` | `mandanten` | Kunde der Anwendung (im Pilot nur BauIn) |
| `Projekt` | `projekte` | z. B. „Elektrifizierung Musterbahn“; Auftraggeber, Vertragsgrundlage, Status, Suchreihenfolge |
| `Abschnitt` | `abschnitte` | PFA bzw. Abschnitt; die Bezeichnung „PFA“ steht am Projekt (`abschnitt_bezeichnung`) |
| `Auftragnehmer` | `auftragnehmer` | Firma oder ARGE; kann in mehreren PFAs Verträge haben |
| `Vertrag` | `vertraege` | Auftrag an einen Auftragnehmer in einem Abschnitt, z. B. „Gleisbau“ |
| `Leistungsverzeichnis` | `leistungsverzeichnisse` | ein LV des Vertrags; Dateien als Dokumente, Importstatus |
| `Nachtrag` | `nachtraege` | Nr., Eingang, Betreff, MKA-Bezug (Liste), Status, zugewiesen an |
| `Nachtragsposition` | `nachtragspositionen` | OZ, Kurztext, Langtext, Menge, Einheit, EP (nur Information) |
| `Dokument` | `dokumente` | hochgeladene Datei; gehört zu Projekt und optional zu LV, Nachtrag usw.; vertraulich ja/nein |
Pivot-Tabellen: `projekt_mitglieder` (Benutzer, Projekt, Projektrolle) und `dokument_freigaben`
(vertrauliche Dokumente für benannte Personen).
## Festlegungen
- **Mandant, Land und Projekt an jedem Fachdatensatz.** `mandant_id` und `land` stehen an allen
Fachtabellen; `projekt_id` an allem unterhalb des Projekts. Die Werte werden beim Speichern vom
übergeordneten Datensatz übernommen (`App\Models\Concerns\ErbtZuordnung`). Ein Datensatz, der
auf einen Datensatz eines anderen Mandanten, Landes oder Projekts zeigt, wird abgewiesen. Das
gilt auch für Vertrag und Auftragnehmer.
- **Rollen:** Globale Rollen (Administrator, Regelverantwortlicher) über Spatie
laravel-permission. Projektrollen (Projektleiter, Bearbeiter, Leser) in `projekt_mitglieder`,
weil sie je Projekt verschieden sind. Der Teams-Modus von Spatie bleibt aus.
- **MKA nur als Bezug:** `nachtraege.mka_bezug` ist eine Liste von Nummern (Fragenkatalog A8).
- **Dokumente polymorph:** `dokumentable` zeigt auf LV, Nachtrag oder Vertrag; `projekt_id`
bleibt für die Rechteprüfung immer gesetzt. Ohne Projekt ist es ein allgemeines Dokument
(Einstellungen). Die Morph-Namen sind fest (`projekt`, `vertrag`, `leistungsverzeichnis`,
`nachtrag`, `user`), damit Klassennamen nicht in der Datenbank stehen.
- **Löschen:** Fachdaten werden nicht kaskadierend gelöscht; ein Projekt mit Abschnitten lässt
sich nicht löschen, sondern wird archiviert. Ausnahmen: Nachtragspositionen hängen am Nachtrag,
Pivot-Einträge an ihren beiden Seiten.
- **Suchreihenfolge je Projekt (vorläufig, Fragenkatalog A10):** andere Verträge im selben
Abschnitt einbeziehen = nein, andere Abschnitte = ja, nur gleicher Auftragnehmer = nein.
- **Beträge** als Dezimalzahlen (Menge 15,3, EP 15,2); Preise sind in Phase 1 nur Information.
+420
View File
@@ -0,0 +1,420 @@
# KI-BauIN – Offene Fragen
Hauptliste aller offenen Fragen mit Status. Die Dateien zum Verschicken (je Empfänger) liegen im
Planungsordner und werden aus dieser Liste erzeugt.
- **Status:** offen → gestellt (Datum) → beantwortet (Datum, Antwort) oder vertagt (Grund)
- **★** = vor dem Termin am 16.10. bzw. vor Beginn des betroffenen Arbeitspakets nötig
- **betrifft** = Teilplan unter `docs/plaene/`
- Kommt eine Antwort, wird sie hier eingetragen. Danach werden die betroffenen Teilpläne angepasst
(Teilplan AP0, Schritt 0.9).
## Übersicht
| ID | Thema | ★ | an | betrifft | Status |
|---|---|---|---|---|---|
| A1 | Gliederung Projekt → PFA → Vertrag → LVs | ★ | BauIn fachlich | AP1, Datenmodell | gestellt (05.10.) |
| A2 | Weitere Vertragsunterlagen im „Topf“ | ★ | BauIn fachlich | AP4, AP5 | gestellt (05.10.) |
| A3 | Vorbemerkungen in X86 oder eigene PDFs | ★ | BauIn fachlich | AP5 | gestellt (05.10.) |
| A4 | Beauftragte Nachträge mit durchsuchen | ★ | BauIn fachlich | AP4, Datenmodell | gestellt (05.10.) |
| A5 | Nachtrags-LV immer als X86/D86 | ★ | BauIn fachlich | AP5 | gestellt (05.10.) |
| A6 | Bezugsposition im Nachtrag genannt | | BauIn fachlich | AP7 | gestellt (05.10.) |
| A7 | Benötigte Teile eines Nachtrags | | BauIn fachlich | AP5 | gestellt (05.10.) |
| A8 | MKA nur als Bezugsnummer | | BauIn fachlich | Datenmodell | gestellt (05.10.) |
| A9 | Ergebnis je Nachtragsposition, Werte | ★ | BauIn fachlich | AP6, AP9 | gestellt (05.10.) |
| A10 | Suchreihenfolge | ★ | BauIn fachlich | AP4 | gestellt (05.10.) |
| A11 | Treffer in anderem PFA mit EP anzeigen | | BauIn fachlich | AP6 | gestellt (05.10.) |
| A12 | Geändert/zusätzlich vorschlagen | | BauIn fachlich | AP7 | gestellt (05.10.) |
| A13 | Anordnungen außen vor | | BauIn fachlich | – | beantwortet (05.10.): außen vor |
| A14 | Bewertungsmatrix (Vorlage, Spalten, Makros) | ★ | BauIn fachlich | AP9, OP1 | gestellt (05.10.) |
| A15 | Stellungnahme BÜW (Vorlage) | | BauIn fachlich | OP2 | vertagt: Phase 2 |
| A16 | DOXIS bleibt Handarbeit | | BauIn fachlich | – | beantwortet (05.10.): von Hand |
| A17 | Mengengerüst Pilot | ★ | BauIn fachlich | AP2, AP11 | gestellt (05.10.) |
| A18 | Anonymisierter Beispielablauf | ★ | BauIn fachlich | AP5 | gestellt (05.10.) |
| A19 | Echte Pilotdaten bei ITM und über die KI-API | ★ | BauIn fachlich | M2 | beantwortet (05.10.): ja, alles bei ITM; ab wann offen |
| A20 | Referenzfälle | | BauIn fachlich | AP2, AP12 | gestellt (05.10.) |
| A21 | Erfolgskriterium | | BauIn fachlich | AP12 | gestellt (05.10.) |
| A22 | Anzahl Nutzer | | BauIn fachlich | AP1 | gestellt (05.10.) |
| A23 | Regeln projektübergreifend | | BauIn fachlich | AP8 | gestellt (05.10.) |
| A24 | Freigabe von Regeln | | BauIn fachlich | AP8 | gestellt (05.10.) |
| A25 | Rollen und Rechtevergabe | ★ | BauIn fachlich | AP1 | gestellt (05.10.) |
| A26 | Österreich | | BauIn fachlich | – | vertagt: Phase 2 |
| A27 | Aufbewahrung nach Projektende | | BauIn fachlich | AP11 | vertagt: nach Livegang Phase 1 |
| A28 | DB-Richtlinien in KI-System (Phase 3) | | BauIn fachlich | Phase 3 | vertagt: nach Livegang Phase 1 |
| B1 | Anmeldung: Microsoft-Konto oder Passwort + 2FA | ★ | BauIn IT | AP1 | beantwortet (05.10.): Passwort, 2FA später Pflicht |
| B2 | Zugriff: Internet oder VPN | ★ | BauIn IT | AP11 | vertagt: vor Livegang |
| B3 | Domain und E-Mail | | BauIn IT | AP10, AP11 | beantwortet (05.10.): keine E-Mails; Adresse vor Livegang |
| B4 | Ordnerstruktur für ZIP-Upload | | BauIn IT | AP1 | beantwortet (05.10.): Mehrfach-Upload, kein ZIP |
| B5 | Backups | | BauIn IT | AP11 | vertagt: später |
| B6 | Datenschutz und Informationssicherheit | ★ | BauIn IT | M2 | vertagt: vor Livegang (Doku in AP11) |
| C1 | Schlüssel je Umgebung, Termin | ★ | ITM | AP3 | beantwortet (05.10.): nur Produktion |
| C2 | Datenverarbeitung bei API-Werk | ★ | ITM | M2 | beantwortet (05.10.): Sache von ITM |
| C3 | Preise für den Kostenrahmen | ★ | ITM | AP0 | beantwortet (05.10.): Sache von ITM |
| C4 | `embed`: Modell, Dimension, Grenzen | | ITM | AP3, AP4 | gestellt (05.10.) |
| C5 | `rerank`: Modell, Grenzen | | ITM | AP3, AP4 | gestellt (05.10.) |
| C6 | `chat`: Modell, Kontext, JSON-Ausgabe | | ITM | AP7 | gestellt (05.10.) |
| C7 | Dokumentdienst: JSON, Dateitypen | | ITM | AP3, AP5 | gestellt (05.10.) |
| C8 | Last und Vorrang | | ITM | AP3 | gestellt (05.10.) |
| C9 | Verfügbarkeit | | ITM | AP11 | gestellt (05.10.) |
| C10 | Server: Ausstattung, Zugang | | ITM | AP11 | beantwortet (05.10.): Sache von ITM |
| C11 | Gitea und Rechte am Code | | ITM | AP0 | beantwortet (05.10.): URL da; Rechte nicht unsere Sache |
| C12 | Abgrenzung mit dem ITM-Entwickler | ★ | ITM | AP3, AP7, AP11 | beantwortet (05.10.): passt |
| C13 | „Datentresor“ bestätigen | | ITM | AP3 | beantwortet (05.10.): passt |
| D2 | Aufwand an ITM für Kostenrahmen | ★ | intern | AP0 | offen |
| D3 | Stand datenschleuse | | intern | AP5 | offen |
| D4 | Freitagstermine | | intern | AP0 | offen |
| D5 | Actions-Runner auf dem Synology-Gitea | ★ | intern | AP1 | offen |
| D6 | Rückmeldung an Claude Design gegeben? | ★ | intern | AP13 | beantwortet (05.10.): Stand 0.3 da |
---
## Teil A – BauIn, Nachtragsbearbeitung (fachlich)
### A1 ★ Gliederung
Wir haben verstanden: Projekt → PFA (Planfeststellungsabschnitt) → Vertrag mit einem
Auftragnehmer (je PFA mehrere, z. B. ARGE, Tunnelbau, Oberleitung) → mehrere LVs (im PFA 4 z. B.
28). Stimmt das? Ist ein Vertrag das, was ihr „Los“ nennt? Gibt es Projekte ohne PFA?
*Antwort:* –
### A2 ★ Was gehört in den „Topf“ eines Vertrags?
Nur die LVs mit Vorbemerkungen, oder auch weitere Vertragsunterlagen? Gemeint sind z. B.
Besondere oder Zusätzliche Vertragsbedingungen, technische Vertragsbedingungen, Baubeschreibung,
Pläne und Verhandlungsprotokolle. Wenn ja: in welcher Form (digitales PDF, Scan)?
*Antwort:* –
### A3 ★ Vorbemerkungen
Stehen sie in den X86-Dateien mit drin, oder liegen sie als eigene PDFs vor?
*Antwort:* –
### A4 ★ Bereits beauftragte Nachträge
Gehören sie zum Bau-Soll und sollen mit durchsucht werden? Sonst würde nicht auffallen, wenn ein
neuer Nachtrag eine Leistung verlangt, die schon über einen früheren Nachtrag beauftragt ist.
*Antwort:* –
### A5 ★ Nachtrags-LV
Gibt es das immer als X86/D86, oder manchmal nur als PDF?
*Antwort:* –
### A6 Bezug auf die Hauptvertragsposition
Nennt der Auftragnehmer bei geänderten Leistungen die Hauptvertragsposition, auf die er sich
bezieht? Das könnte z. B. eine OZ im Langtext, in der Kalkulation oder im Anschreiben sein. Dann
prüft das System diesen Bezug, statt ihn nur zu suchen.
*Antwort:* –
### A7 Benötigte Teile eines Nachtrags
Vorschlag: Für die Prüfung dem Grunde nach liest das System das Nachtrags-LV und das Anschreiben
(Sachverhalt). Kalkulation und Preisnachweise werden nur abgelegt und erst in Phase 2 ausgewertet.
Passt das?
*Antwort:* –
### A8 MKA
Vorschlag: Am Nachtrag wird nur die MKA-Nummer vermerkt (auch mehrere), die MKA selbst wird nicht
geprüft. Passt das, oder sollen MKAs mit Dokument im System liegen?
*Antwort:* –
### A9 ★ Ergebnis je Nachtragsposition
Vorschlag: Die KI liefert:
- „Im Vertrag enthalten: ja / teilweise / nein / unklar“
- die Fundstellen (LV, OZ oder Vorbemerkung, mit Sprung ins PDF)
- eine kurze Begründung
- bei geänderter Leistung die Bezugsposition und den Unterschied, z. B. „Schwelle B70“ im LV,
„Schwelle B70 besohlt“ im Nachtrag
Das Prüfergebnis (dem Grunde nach berechtigt / nicht berechtigt / teilweise) setzt der Bearbeiter.
Passt das? Welche Werte verwendet ihr in der Matrix?
*Antwort:* –
### A10 ★ Suchreihenfolge
1. LVs des betroffenen Vertrags.
2. Übrige Verträge im selben PFA? Etwa wenn die Leistung schon an einen anderen Auftragnehmer
vergeben ist.
3. Andere PFAs desselben Projekts als Hinweis. Nur beim selben Auftragnehmer oder bei allen?
Andere Projekte nie. Richtig so?
*Antwort:* –
### A11 Treffer in einem anderen PFA (Beispiel PFA 3/4)
Vorschlag: Das System zeigt die Fundstelle mit Auftragnehmer und Einheitspreis als Hinweis an,
ohne den Preis zu bewerten. Die Preisprüfung kommt in Phase 2. Gewünscht?
*Antwort:* –
### A12 Geändert oder zusätzlich
Soll die KI vorschlagen, ob eine geänderte (§ 2 Abs. 5) oder eine zusätzliche Leistung (§ 2 Abs. 6)
vorliegt? Das ergibt sich fast von selbst: Ist eine Bezugsposition gefunden, ist die Leistung
geändert. Die rechtliche Einordnung macht aber BauIn. Also weglassen oder als Hinweis zeigen?
*Antwort:* –
### A13 Anordnungen des Auftraggebers
Gemeint ist z. B. eine E-Mail der DB-Projektleitung zu besohlten Schwellen. Vorschlag: Anordnungen
bleiben in Phase 1 außen vor. Einverstanden?
*Antwort:* beantwortet (05.10.): Passt. Anordnungen bleiben in Phase 1 außen vor.
### A14 ★ Bewertungsmatrix
Bitte die leere Vorlage und 2–3 ausgefüllte Matrizen schicken.
- Welche Spalten und Reiter füllt ihr bei der Prüfung dem Grunde nach aus?
- Ist es eine Vorlage der DB mit Makro (D86-Import, Datei .xlsm)?
- Ändert die DB die Vorlage gelegentlich?
*Antwort:* –
### A15 Stellungnahme BÜW
Bitte die Vorlage schicken. Ist sie .doc oder .docx? Wir können nur .docx automatisch füllen; eine
einmalige Umwandlung reicht. Welche Felder ändern sich, z. B. Ansprechpartner DB, Bearbeiter,
Nachtrag-Nr., Betreff, Angebotssumme, Sachverhalt?
*Antwort:* vertagt (05.10.): Die Stellungnahme BÜW kommt erst in Phase 2. OP2 entfällt in Phase 1.
### A16 DOXIS
Vorschlag: Ergebnis und Stellungnahme lädt der Bearbeiter weiter von Hand in DOXIS hoch, ohne
Anbindung in Phase 1. Einverstanden?
*Antwort:* beantwortet (05.10.): Passt. Der Bearbeiter lädt Ergebnis und Stellungnahme weiter von Hand in DOXIS hoch.
### A17 ★ Mengengerüst Pilot
- Anzahl Verträge und LVs je PFA
- Ungefähre Zahl der Positionen je LV, Größe der X86- und PDF-Dateien
- Wie viele Nachträge kommen pro Monat, mit wie vielen Positionen?
Wir brauchen das für Server, API-Kosten und den Kostenrahmen.
*Antwort:* –
### A18 ★ Anonymisierter Beispielablauf
Gewünscht sind ein Vertrag mit 2–3 LVs (X86 + PDF), 2–3 Nachträge mit Nachtrags-LV und Anschreiben
sowie die ausgefüllte Matrix. Firmen, Namen und Bankdaten dürfen ersetzt oder geschwärzt sein.
- Bis wann geht das?
- Was darf auf keinen Fall in Entwicklungswerkzeuge gelangen?
*Antwort:* –
### A19 ★ Echte Pilotdaten
- Dürfen die Pilotdaten auf den Servern von ITM liegen und über die KI-API von ITM verarbeitet werden?
- Muss die DB zustimmen, z. B. wegen Geheimhaltungspflichten aus eurem Vertrag mit der DB?
- Ab wann ginge das?
*Antwort:* beantwortet (05.10.): Ja. Alle Daten liegen final bei ITM; BauIn hat keine eigene
Serverstruktur. Offen: ab wann. Für Claude Code gilt weiter: keine echten Daten.
### A20 Referenzfälle
Bitte 10–20 abgeschlossene Nachträge mit eurem Ergebnis, damit wir die Trefferquote messen können.
*Antwort:* –
### A21 Erfolg
Vorschlag: Bei mindestens 80 % der Nachtragspositionen steht die richtige Fundstelle unter den
ersten drei Treffern, und die Prüfzeit je Nachtrag halbiert sich. Wie lange dauert ein Nachtrag
heute, LV-Suche und Matrix zusammen?
*Antwort:* –
### A22 Nutzer
Wie viele Bearbeiter arbeiten mit dem System? Wer gibt Rückmeldung zu den KI-Vorschlägen?
*Antwort:* –
### A23 Regeln aus Korrekturen
Sollen Regeln aus Korrekturen auch projektübergreifend gelten dürfen? Ein Beispiel wäre
„Besohlung ist in einer Standard-Schwellenposition nicht enthalten“. Oder strikt je Projekt?
Für LVs habt ihr gesagt: nie projektübergreifend.
*Antwort:* –
### A24 Freigabe von Regeln
Vorschlag bei wenigen Nutzern: „Ohne Freigabe“, alles wird protokolliert. Später lässt sich auf
„Abgestuft“ oder „Streng“ umstellen. Einverstanden?
*Antwort:* –
### A25 ★ Rollen
Vorschlag:
- Rollen: Administrator, Projektleiter, Bearbeiter, Leser, dazu optional ein Regelverantwortlicher
- Rechte je Projekt; vertrauliche Einzeldokumente nur für benannte Personen
- Der Administrator sieht Projektinhalte nur, wenn er Projektmitglied ist
Passt das? Wer vergibt die Rechte?
*Antwort:* –
### A26 Österreich
Ist Österreich noch ein Thema, und wenn ja, wann?
*Antwort:* vertagt (05.10.): Österreich kommt in Phase 2.
### A27 Aufbewahrung
Das Pilotprojekt läuft bis etwa 2032. Was passiert danach mit den Daten (archivieren, löschen,
Fristen)?
*Antwort:* vertagt (05.10.): wird nach dem Livegang von Phase 1 geklärt.
### A28 Phase 3, nicht eilig
Dürfen die Richtlinien der DB nach den Nutzungsbedingungen eures Regelwerk-Bezugs in ein
KI-System übernommen werden?
*Antwort:* vertagt (05.10.): wird nach dem Livegang von Phase 1 geklärt.
---
## Teil B – BauIn, IT
### B1 ★ Anmeldung
BauIn arbeitet mit Teams. Soll die Anmeldung über das Microsoft-Konto (Entra ID) laufen, oder mit
eigenem Passwort und 2FA? Soll 2FA Pflicht sein?
*Antwort:* beantwortet (05.10.): Eigenes Passwort, zunächst ohne 2FA. 2FA wird später
eingeschaltet und ist dann Pflicht. Keine Anmeldung über das Microsoft-Konto.
### B2 ★ Zugriff
Die Anwendung läuft auf Servern von ITM. Soll sie aus dem Internet mit Login und 2FA erreichbar
sein, oder nur über VPN bzw. von festen IP-Adressen?
*Antwort:* vertagt (05.10.): wird kurz vor dem Livegang geklärt, für die Entwicklung nicht relevant.
### B3 Domain und E-Mail
Unter welcher Adresse soll die Anwendung laufen? Sollen Benachrichtigungen per E-Mail kommen, und
von welchem Absender?
*Antwort:* beantwortet (05.10.): Keine E-Mails. Die Adresse wird kurz vor dem Livegang geplant.
### B4 Dateien ins System bringen
Vorschlag: Massen-Upload als ZIP mit eurer Ordnerstruktur (PFA → Vertrag → LVs); das System ordnet
PDF und X86 paarweise zu. Wie sind die Ordner auf eurem Server benannt? Gibt es eine feste
Struktur?
*Antwort:* beantwortet (05.10.): Massen-Upload mehrerer Dateien, aber kein ZIP. Die Ordner werden
manuell angelegt, eine feste Struktur gibt es nicht.
### B5 Backups
Welche Anforderungen gibt es an Aufbewahrung und Speicherort? Wer ist zuständig (mit ITM klären)?
*Antwort:* vertagt (05.10.): wird später geklärt.
### B6 ★ Datenschutz und Informationssicherheit
Gibt es Vorgaben von BauIn oder von der DB, z. B. Sicherheitsanforderungen an Auftragnehmer der
DB, ISO 27001 oder Geheimhaltung? Wer muss der Verarbeitung über die KI-API zustimmen?
*Antwort:* vertagt (05.10.): Wichtig, aber für die jetzige Entwicklung (Prototyp) noch nicht relevant.
In die Doku aufgenommen: AP11, „Vor dem Livegang zu klären“.
---
## Teil C – ITM / API-Werk
### C1 ★ Schlüssel
Gibt es getrennte Schlüssel für Entwicklung, Staging und Produktion, mit eigener Zählung und
eigenem Limit? Wann bekommen wir die Schlüssel für die Entwicklung?
*Antwort:* beantwortet (05.10.): Es gibt nur Schlüssel für die Produktion. Entwicklung, Staging und
Produktion teilen sich damit Zählung, Kosten und Limit.
### C2 ★ Datenverarbeitung
- Wo laufen api-werk.de, der Extractor und der KI-Server (Land, Rechenzentrum, eigene Hardware)?
- Die Doku erwähnt zettellos.ai: Sieht ein Dritter die Dokumente?
- Wie lange werden hochgeladene PDFs, Ergebnisse, Prompts und Logs gespeichert? Lässt sich ein
Auftrag löschen?
- Gibt es einen Vertrag zur Auftragsverarbeitung? Werden Daten zum Training genutzt?
*Antwort:* beantwortet (05.10.): Darum kümmert sich ITM, nicht unser Teil.
### C3 ★ Preise
Wie hoch sind die Preise je Seite (Dokumentdienst) und je Anfrage bzw. Token (KI)? Wir brauchen
sie für den Kostenrahmen an BauIn bis 16.10. Wer erstellt den Kostenrahmen, und welche Zahlen
braucht ihr von uns bis wann?
*Antwort:* beantwortet (05.10.): Preise und Kostenrahmen der KI-API sind Sache von ITM, nicht unser Teil.
### C4 `embed`
- Welches Modell steckt dahinter (Qwen3-Embedding-8B?), mit welcher Dimension (4.096?)?
Sind die Vektoren normalisiert?
- Wie viele Tokens je Text und wie viele Texte je Anfrage sind möglich?
- Ergänzt die API bei Suchanfragen die Anweisung („Instruct: …“), oder machen wir das?
- Wie erfahren wir, wenn sich das Modell hinter dem Alias ändert?
*Antwort:* –
### C5 `rerank`
Welches Modell steckt dahinter? Wie viele Dokumente je Anfrage sind möglich, und wie lang darf ein
Dokument sein?
*Antwort:* –
### C6 `chat` und `chat-noreasoning`
- Welche Modelle stecken dahinter?
- Wie lang ist der Kontext (128K?) einschließlich Ausgabe, und wie lang darf die Ausgabe sein?
- Gibt es strukturierte Ausgabe (`response_format` mit JSON-Schema) oder Tool-Aufrufe?
- Lassen sich `temperature` und `seed` setzen?
- Was ist mit „Ziel 1 Milliarde“ aus der Besprechung gemeint, und ab wann?
*Antwort:* –
### C7 Dokumentdienst
- Wie sieht die JSON-Ausgabe aus (Seitenzahlen, Tabellen, Koordinaten)?
- Welche Dateitypen gehen außer PDF?
- Gibt es eine maximale Seitenzahl?
- Wie lange bleiben Aufträge abrufbar?
*Antwort:* –
### C8 Last und Vorrang
Die 120 Anfragen pro Minute je Schlüssel gelten für alle Nutzer gemeinsam. Können interaktive
Suchanfragen Vorrang vor dem Einlesen großer Mengen bekommen, z. B. über getrennte Schlüssel?
Lässt sich das Limit für die Erstindexierung zeitweise erhöhen?
*Antwort:* –
### C9 Verfügbarkeit
Gibt es Wartungsfenster und eine Statusseite? Wer ist Ansprechpartner bei Störungen?
*Antwort:* –
### C10 Server für Staging und Produktion
- Ausstattung und Betriebssystem, siehe `docs/server-anforderungen.md`
- Wer hat Root-Zugang? Ist die Festplatte verschlüsselt? Wie läuft die Verbindung zu api-werk.de?
- Wie werden die Server gesichert?
*Antwort:* beantwortet (05.10.): Sache von ITM (Ausstattung nach unseren Server-Anforderungen,
Zugang, Verschlüsselung, Sicherung).
### C11 Gitea
Wie lautet die Adresse für den Push-Mirror? Wem gehört der Code, und wer hat welche
Nutzungsrechte?
*Antwort:* beantwortet (05.10.): Push-Mirror nach https://gitea.itm-technologies.de/ChristophGraf/BauIN.
Rechte am Code: nicht unsere Sache.
### C12 ★ Abgrenzung mit dem ITM-Entwickler
Vorschlag:
- Er betreut die KI-API und baut die Server für Staging und Produktion nach unseren Vorgaben.
- Wir bauen die gesamte Anwendung, einschließlich API-Anbindung, Indexierung, Suche, Prompts und
Prüflogik.
- Er berät zu Modellen, Grenzen und Prompts.
Passt das? Wie viel Zeit hat er für das Projekt, und wer übernimmt Deployment und Updates im
Betrieb?
*Antwort:* beantwortet (05.10.): Passt. Nicht beantwortet: Zeit des ITM-Entwicklers und wer im Betrieb
deployt (AP11, E11.1).
### C13 „Datentresor“
Wir haben den Wunsch so umgesetzt: Die Schlüssel liegen in der Windows-Anmeldeinformationsverwaltung,
ein lokaler Proxy setzt sie ein, und in der Anwendung stehen sie verschlüsselt in der Datenbank,
nie in der `.env`. War das so gemeint?
*Antwort:* beantwortet (05.10.): Passt, so war es gemeint.
---
## Teil D – intern (du)
### D2 ★ Aufwand für den Kostenrahmen
Ist der Aufwand an die ITM-Projektleitung gegangen? Zahlen: Kern etwa 11 PW mit Claude Code,
mit Puffer 13–14 PW, Optionen je 0,5 PW, dazu 1–1,5 PW des ITM-Entwicklers.
*Antwort:* –
### D3 datenschleuse
Wie ist der Stand? Sie hilft beim anonymisierten Beispielablauf.
*Antwort:* –
### D4 Freitagstermine
Vorschlag: 16.10., 30.10. (M1), 20.11. (M2), 04.12., 18.12. (M3), 15.01., 29.01. (M4). Passt das?
*Antwort:* –
### D5 ★ Actions-Runner
Läuft auf dem Synology-Gitea schon ein Actions-Runner? Wenn nicht, schreibt Claude Code ein
Einrichtungsskript.
*Antwort:* –
### D6 ★ Claude Design
Hast du die Rückmeldung (`Rueckmeldung_an_ClaudeDesign_2026-10-02.md`) schon an Claude Design
gegeben? Den neuen Stand (Übergabe und `.dc.html`-Dateien) brauchen wir bis KW 42 für AP13.
*Antwort:* beantwortet (05.10.): Rückmeldung gegeben. Stand 0.3 vom 05.10. mit den Änderungen 1–26 liegt
im Planungsordner unter `claude-design/2026-10-05_Stand-0.3/`; Abgleich in AP13.
---
## Bereits beantwortet
- **Format der LVs:** X86, D86 und PDF, immer alle drei, aus dem Kalkulationsprogramm (02.10.)
- **Ablauf bei BauIn:** in der Besprechung am Bildschirm gezeigt (02.10.)
- **Sprachmodell:** vorhanden (`chat`, `chat-noreasoning`), Kontext laut ITM 128K (02.10.)
- **API-Dokumentation:** liegt vor. Keine Webhooks, 120 Anfragen/min, 50 MB je Datei (02.10.)
- **Ansprechpartner:** fachlich die Nachtragsbearbeitung, IT eine eigene Ansprechpartnerin;
Teams, Termine freitags (02.10.)
- **Prüfung der Höhe nach:** Phase 2; bei der DB prüft der Einkauf mit der versiegelten
Urkalkulation (02.10.)
- **Server:** stellt ITM (02.10.)
- **D1 Team:** du + Claude Code (Vibe Coding), ITM-Entwickler für KI-API und Server; du arbeitest
rund 30 h pro Woche, erhöhbar (02.10.)
+68
View File
@@ -0,0 +1,68 @@
# AP0 – Steuerung, Klärung, Doku
| | |
|---|---|
| Status | in Arbeit |
| Aufwand | 0,75 PW, laufend |
| Zeitraum | KW 40 – KW 4/2027 |
| Meilenstein | alle |
| Freigabe | – (laufende Aufgabe) |
## Ziel
Offene Fragen werden geklärt, Plan und Doku sind aktuell, und BauIn und ITM wissen zu jedem
Termin, wo das Projekt steht.
## Umfang
**Enthalten:**
- Fragen stellen und Antworten einpflegen
- Statusberichte und Termine mit BauIn
- Gesamtplan, Teilpläne und Doku pflegen
- Abstimmung mit ITM und Claude Design
**Nicht enthalten:** Umsetzung, die steht in den übrigen Arbeitspaketen.
## Voraussetzungen
Keine.
## Schritte
- [x] **0.1** Besprechung mit BauIn ausgewertet, API-Doku gelesen → Plan vom 02.10.
- [x] **0.2** Gesamtplan neu gegliedert, Teilpläne angelegt, Fragenliste mit Status
(`docs/fragen.md`) → dieser Stand
- [ ] **0.3** Du gibst Gesamtplan und Teilpläne frei (oder korrigierst sie) → Status in den Teilplänen
- [x] **0.4** Fragen verschicken (du), erledigt am 05.10. Erste Antworten (B1–B6, C1–C3, C10–C13,
A13, A15, A16, A19, A26–A28) sind in `docs/fragen.md` und den Teilplänen eingearbeitet.
- Teil A an die Nachtragsbearbeitung von BauIn
- Teil B an die IT von BauIn
- Teil C an die ITM-Projektleitung
Die Dateien zum Verschicken liegen im Planungsordner. Danach steht der Status in
`docs/fragen.md` auf „gestellt“.
- [ ] **0.5** Server-Anforderungen an den ITM-Entwickler schicken (du) → Bestätigung oder Rückfragen
- [x] **0.6** Rückmeldung an Claude Design geben (du); den neuen Stand ablegen, sobald er da ist
→ Stand 0.3 vom 05.10. in `claude-design/2026-10-05_Stand-0.3/` im Planungsordner, Abgleich in AP13
- [ ] **0.6a** Push-Mirror zum Gitea von ITM einrichten (du, Weboberfläche des lokalen Gitea)
→ `https://gitea.itm-technologies.de/ChristophGraf/BauIN`, erster Abgleich geprüft
- [ ] **0.7** Aufwand für den Kostenrahmen an die ITM-Projektleitung (du, bis 14.10.)
→ Zahlen aus Abschnitt 4 des Gesamtplans
- [ ] **0.8** Statusbericht für den 16.10.: Claude Code entwirft bis 14.10., du ergänzt Screenshots
→ `docs/status/2026-10-16.md`
- [ ] **0.9** Termin 16.10. durchführen (du); Antworten in `docs/fragen.md` eintragen, betroffene
Teilpläne und Datenmodell anpassen (Claude Code, KW 42–43) → aktualisierte Pläne zur Freigabe
- [ ] **0.10** Laufend:
- Teilplan spätestens eine Woche vor Start detaillieren
- Statusbericht vor jedem Freitagstermin
- Änderungsprotokoll des Gesamtplans pflegen
## Abnahmekriterien
- Alle ★-Fragen sind beantwortet oder bewusst vertagt.
- Der Plan entspricht vor jedem Termin dem tatsächlichen Stand.
## Risiken
- Antworten kommen spät. Dann mit dem Vorschlag aus der Frage weiterarbeiten und das im Plan
vermerken.
+136
View File
@@ -0,0 +1,136 @@
# AP1 – Fundament: CI, Rechte, Benutzer, Projekte, Upload, Protokoll
| | |
|---|---|
| Status | in Arbeit; Schritte ab 1.5 detailliert, warten auf Freigabe |
| Aufwand | 1,25 PW offen (mit Claude Code) |
| Zeitraum | KW 40–43 |
| Meilenstein | M1 (30.10.) |
| Freigabe | offen |
## Ziel
Benutzer melden sich an, sehen nur die Projekte, in denen sie Mitglied sind, legen Projekte mit
PFA und Verträgen an und laden LV-Pakete hoch. Jede wichtige Aktion wird protokolliert.
## Umfang
**Enthalten:**
- CI
- Rechtekonzept und Policies
- Benutzerverwaltung
- Projektverwaltung (Projekt, PFA, Verträge, Auftragnehmer, Mitglieder)
- Upload (mehrere Dateien auf einmal, kein ZIP) und Download im privaten Speicher
- Protokoll
- Länderstruktur
**Nicht enthalten:**
- GAEB-Import (AP5)
- Anmeldung mit dem Microsoft-Konto (entfällt, B1)
- E-Mails jeder Art, auch Einladung und „Passwort vergessen“ (B3)
- Endgültiges Design (AP13): Die Oberflächen entstehen zuerst mit TallStackUI-Standard und
werden in AP13 angepasst.
## Voraussetzungen
- Fragen: A1 (Gliederung), A25 (Rollen). Beantwortet am 05.10.: B1 (eigenes Passwort, 2FA später
Pflicht), B3 (keine E-Mails), B4 (Mehrfach-Upload, kein ZIP, Ordner von Hand)
- D5 (Actions-Runner auf dem Synology-Gitea)
## Entscheidungen
| Nr. | Frage | Vorschlag | Entschieden |
|---|---|---|---|
| E1.1 | Projektrollen über eigene Tabelle oder Spatie-Teams? | Eigene Tabelle `projekt_mitglieder` (umgesetzt); globale Rollen über Spatie | offen |
| E1.2 | Protokoll: Paket oder eigene Tabelle? | `spatie/laravel-activitylog` (MIT, verbreitet) | offen |
| E1.3 | Neue Benutzer und vergessene Passwörter ohne E-Mail (B3) | Administrator vergibt ein Startpasswort, das bei der ersten Anmeldung geändert werden muss; er setzt auch vergessene Passwörter so zurück. „Passwort vergessen“ zeigt nur den Hinweis auf den Administrator | offen |
| E1.4 | Benutzer löschen oder nur deaktivieren? | Nur deaktivieren, damit Protokoll und Zuordnungen erhalten bleiben | offen |
| E1.5 | Darf der Administrator Projektinhalte sehen? | Nein, nur wenn er Projektmitglied ist | offen (A25) |
| E1.6 | 2FA-Pflicht (B1) | Schalter unter Verwaltung → Einstellungen, zunächst aus. Eingeschaltet muss jeder Benutzer 2FA einrichten, bevor er weiterarbeitet; der Administrator kann 2FA eines Benutzers zurücksetzen | offen |
## Schritte
- [x] **1.1** Entwicklungsumgebung, Repository, gitleaks-Hook
- [x] **1.2** Laravel 13, Livewire 4, TallStackUI 4, Fortify (2FA, Passkeys), Spatie,
Mehrsprachigkeit
- [x] **1.3** Nicht-funktionale Grundlagen: keine externen Ressourcen (Test), `CLAUDE.md`, Tech-Stack-Doku
- [x] **1.4** Datenmodell Kern (`docs/datenmodell.md`). Umgesetzt vor Freigabe.
- [ ] **1.4a** Review mit dir: Erklärung der Tabellen, Entscheidung E1.1
- [ ] **1.4b** Anpassen nach den Antworten A1, A4, A8 und A10 und an den Prototyp 0.3
(AP13, Abgleich): Projekt ohne Vertragsgrundlage im Formular, Suchbereich nur ein Schalter
„Andere Verträge im selben PFA einbeziehen“, Vertrag mit Gewerk und „Vertrag vom“
- [ ] **1.5** CI in Gitea Actions:
- Runner auf dem Synology prüfen bzw. einrichten (Skript von Claude Code, du führst es aus).
- Workflow mit `composer install`, `npm run build`, Tests gegen MySQL, Pint (`--test`),
PHPStan, Proxy-Tests und gitleaks.
→ Ergebnis: Jeder Push zeigt grün oder rot.
- [ ] **1.6** Rechtekonzept `docs/rechte.md` → Ergebnis: Rechte-Matrix zur Freigabe durch dich (vorläufig bis A25):
- Rollen: global Administrator (optional Regelverantwortlicher); je Projekt Projektleiter,
Bearbeiter, Leser
- Matrix Aktion × Rolle: Projekt sehen, anlegen, bearbeiten, archivieren; Mitglieder verwalten;
Dokumente hochladen, sehen, herunterladen; vertrauliche Dokumente; Nachträge anlegen,
zuweisen, prüfen; Export; Regeln vorschlagen und freigeben; Verwaltung
- Mandantentrennung und Regeln für archivierte Projekte (nur lesen)
- [ ] **1.7** Policies und Gates nach dem Rechtekonzept:
- zentrale Abfrage „Projekte des Benutzers“
- Mandanten-Scope
- vertrauliche Dokumente nur für freigegebene Personen
→ Ergebnis: Je Aktion ein Test mit und einer ohne Berechtigung.
- [ ] **1.8** Benutzerverwaltung (Administrator):
- Liste mit Suche
- Anlegen mit Startpasswort (E1.3), Bearbeiten, Passwort zurücksetzen, Deaktivieren (E1.4)
- Globale Rolle, Mandant, 2FA-Status; Schalter 2FA-Pflicht (E1.6)
→ Ergebnis: Verwaltung → Benutzer.
- [ ] **1.9** Projektverwaltung:
- Projektliste
- Projekt anlegen in drei Schritten wie im Prototyp „Projekt anlegen v3“: Projektdaten mit PFA
und Verträgen, Vertragsunterlagen (Upload aus 1.10), Prüfen und anlegen
- Mitglieder im Reiter „Berechtigungen“ des Projektdetails
- Bearbeiten, Archivieren (schreibgeschützt)
→ Ergebnis: Projekte → Neues Projekt.
- [ ] **1.10** Upload und Download:
- Speicher `storage/app/private/<mandant>/<projekt>/…`
- Mehrere Dateien auf einmal hochladen (Auswahl oder Ziehen), kein ZIP (B4); bis 200 MB je Datei
- Paarbildung PDF, X86, D86 nach Dateinamen. Die Ordner bei BauIn haben keine feste Struktur
(B4), deshalb werden PFA und Vertrag in der Anwendung gewählt bzw. je LV bestätigt
- Prüfung von Dateityp und Größe, Erkennung von Duplikaten (sha256)
- Download nur über eine Route mit Rechteprüfung
→ Ergebnis: LV-Paket hochladen, LVs als Datensätze mit Dokumenten.
- [ ] **1.11** Protokoll (E1.2):
- Anmeldung (Erfolg und Fehlschlag)
- Benutzer- und Rechteänderungen
- Projektänderungen
- Upload, Löschen, Herunterladen vertraulicher Dokumente
- Einstellungen und Exporte
→ Ergebnis: Verwaltung → Protokoll mit Filter.
- [ ] **1.12** Länderstruktur:
- Schnittstelle für länderspezifische Begriffe und Auswahllisten
- Umsetzung für DE, AT nur als Platzhalter
- Länderwahl nur bei mehr als einem freigeschalteten Land
## Abnahmekriterien
- Die Kriterien von M1 zu Benutzern, Projekten, Upload, Rechten und Protokoll sind erfüllt
(`docs/projektplan.md`, Abschnitt 5).
- Ein Benutzer ohne Mitgliedschaft erhält auf jeder Projekt-, Dokument- und Download-URL 403 oder 404.
- CI ist grün.
## Tests
- Feature-Tests je Aktion mit und ohne Berechtigung, auch für andere Mandanten.
- Upload: Paarbildung, zu große Datei, falscher Typ, Duplikat, viele Dateien auf einmal.
- Anmeldung: Startpasswort muss geändert werden; mit eingeschalteter 2FA-Pflicht kein Zugriff
ohne eingerichtete 2FA.
- Protokolleinträge für die genannten Ereignisse.
## Risiken
- Die Rollen ändern sich nach A25. Deshalb das Konzept zuerst schriftlich und die Policies zentral halten.
- Große Dateien und viele Dateien auf einmal: Upload-Grenzen in PHP, Nginx und Livewire
abstimmen und testen.
+67
View File
@@ -0,0 +1,67 @@
# AP2 – Meilisearch-Test
| | |
|---|---|
| Status | detailliert, wartet auf Freigabe |
| Aufwand | 0,25 PW |
| Zeitraum | Teil 1 KW 41, Teil 2 KW 46 |
| Meilenstein | M2 |
| Freigabe | offen |
## Ziel
Belegen, ob Meilisearch v1.54.2 Vektoren mit 4.096 Dimensionen schnell genug durchsucht. Danach
steht fest, wie viel RAM der Server braucht und ob die binäre Quantisierung nötig ist. Teil 2
misst die Trefferqualität mit echten Embeddings.
## Umfang
**Enthalten:**
- Teil 1: Leistung mit erzeugten Zufallsvektoren
- Teil 2: Trefferquote mit echten Vektoren und Referenzfällen
**Nicht enthalten:** Aufbau der echten Indexierung (AP4).
## Voraussetzungen
- Teil 1: keine
- Teil 2: Staging, Pilotdaten, Referenzfälle (A20), KI-API (AP3)
## Entscheidungen
| Nr. | Frage | Vorschlag | Entschieden |
|---|---|---|---|
| E2.1 | Grenzwerte für „bestanden“ | Bei 50.000 Abschnitten mit Filter auf Vertrag: Antwortzeit p95 < 300 ms, Indexierung < 30 min, RAM von Meilisearch < 8 GB | offen |
## Schritte
**Teil 1 (KW 41):**
- [ ] **2.1** Messskript (`tools/meili-test/`, nur Entwicklung):
- erzeugt N Abschnitte mit normalisierten Zufallsvektoren (4.096 Dimensionen)
- mit Filterfeldern: Mandant, Land, Projekt, Abschnitt, Vertrag, Art
- mit kurzen Texten
- [ ] **2.2** Messung ohne Quantisierung für N = 10.000 und 50.000:
- Indexierungszeit, Plattenbedarf, RAM
- Antwortzeit p50/p95 über je 100 Anfragen für reine Vektorsuche, hybride Suche, hybride
Suche mit Filter auf Vertrag bzw. Projekt
- [ ] **2.3** Dieselbe Messung mit `binaryQuantized` in einem eigenen Index
- [ ] **2.4** Bericht `docs/tests/meilisearch-teil1.md` → Ergebnis: Empfehlung zu RAM für die
Server-Anforderungen und vorläufig zur Quantisierung; bei Nichtbestehen Plan B (Qdrant) bewerten
**Teil 2 (KW 46):**
- [ ] **2.5** Messskript für Staging. Es gibt nur Kennzahlen aus, keine Inhalte:
- Recall@10 und Recall@40 der richtigen Fundstelle
- jeweils vor und nach dem Reranker
- mit und ohne Quantisierung
- [ ] **2.6** Du führst es auf Staging aus. Bericht `docs/tests/meilisearch-teil2.md` →
Ergebnis: Entscheidung zur Quantisierung und Kandidatenzahl für AP4
## Abnahmekriterien
- Beide Berichte liegen vor und enthalten eine Empfehlung.
- Die Server-Anforderungen sind bei Bedarf angepasst.
## Risiken
- Zufallsvektoren sagen nichts über die Trefferqualität. Dafür gibt es Teil 2.
- Große JSON-Mengen beim Indexieren: in Stapeln senden.
+92
View File
@@ -0,0 +1,92 @@
# AP3 – Anbindung KI-API ITM
| | |
|---|---|
| Status | in Arbeit; Schritte ab 3.2 detailliert, warten auf Freigabe |
| Aufwand | 0,5 PW offen |
| Zeitraum | KW 42 (Schnelltest, Einstellungen), KW 44 (Clients, Jobs) |
| Meilenstein | M1/M2 |
| Freigabe | offen |
## Ziel
Die Anwendung nutzt Dokumentdienst, `embed`, `rerank` und `chat` zuverlässig. Sie beachtet die
Grenzen der API und protokolliert jeden Aufruf ohne Inhalte und ohne Schlüssel. Die Schlüssel
liegen verschlüsselt in der Datenbank.
## Umfang
**Enthalten:**
- Schlüsselverwaltung, Clients, Hintergrund-Jobs, Ratenbegrenzung, Wiederholungen,
Verbrauchsprotokoll, Statusprüfung
**Nicht enthalten:**
- Indexierung (AP4)
- Prompts für die Beurteilung (AP7)
## Voraussetzungen
- C4–C8 (Modelle, Grenzen); Schlüssel im Windows-Tresor, Proxy läuft (du)
- C1 (05.10.): Es gibt nur Schlüssel für die Produktion. Entwicklung, Staging und Produktion
teilen sich Zählung, Kosten und das Limit von 120 Anfragen/min. Deshalb:
- Automatische Tests und CI rufen die echte API nie auf (`Http::fake`).
- Live-Aufrufe nur im Schnelltest (3.2) und gezielt, mit kleinen Testtexten.
- Getrennte Schlüssel für Vorrang (C8) sind nicht zu erwarten; der Ratenbegrenzer (E3.1) muss
das allein leisten.
- AP1 Schritt 1.7 (Rechte für Verwaltung)
## Entscheidungen
| Nr. | Frage | Vorschlag | Entschieden |
|---|---|---|---|
| E3.1 | Anteil der 120 Anfragen/min für interaktive Suche | 40/min reserviert, Rest für Hintergrund-Jobs; nach C8 anpassen | offen |
| E3.2 | Standardmodell für einfache Aufgaben | `chat-noreasoning`, `chat` nur für die Beurteilung (AP7) | offen |
## Schritte
- [x] **3.1** Schlüssel-Proxy (`tools/ki-proxy`), Sperr-Hook für Claude Code, gitleaks-Regel
- [ ] **3.2** Schnelltest über den Proxy, wenn du ihn gestartet hast. Nur Testtexte:
- Modellverzeichnis
- Dimension und Normalisierung von `embed` messen
- `rerank`
- `chat` mit Streaming
→ `docs/tests/ki-api-schnelltest.md`
- [ ] **3.3** Einstellungen KI-Schnittstelle:
- Tabelle mit verschlüsselten Feldern
- Verwaltungsmaske: Basis-URL, drei Schlüsselfelder (nur beschreibbar, Anzeige `zki_…a1b2`),
Modellnamen, „Verbindung prüfen“
- Protokolleintrag bei Änderung (ohne Wert)
- In der Entwicklung zeigt die Basis-URL auf den Proxy, die Schlüssel bleiben leer.
- [ ] **3.4** Clients:
- `Dokumentdienst` (einreichen, Status, Ergebnis Markdown/JSON)
- `Embeddings` (Stapel; Instruct-Präfix für Suchanfragen nach C4)
- `Rerank`
- `Chat` (normal und Streaming)
- [ ] **3.5** Jobs und Ausfallsicherheit:
- Ergebnisse abfragen im Abstand von 2 bis 10 s
- Job-ID gleich nach dem Einreichen speichern, keine Doppel-Einreichung nach Timeout
- Wiederholung bei 429 (Retry-After) und 5xx mit Backoff
- Ratenbegrenzer nach E3.1
- [ ] **3.6** Verbrauchsprotokoll `ki_aufrufe`: Dienst, Modell, Dauer, Status, Tokens, Bezug.
Keine Inhalte, keine Header.
- [ ] **3.7** Statusprüfung: Artisan-Befehl und Anzeige im Systemstatus (erreichbar, Antwortzeit,
Auslastung); Alarm über AP11
## Abnahmekriterien
- „Verbindung prüfen“ zeigt alle Dienste grün.
- Die Schlüssel stehen nirgends im Klartext: nicht in der DB, nicht im Log, nicht in der Antwort
der Maske.
- Ein Ausfall der API stoppt keine Seite; die Jobs laufen später weiter.
## Tests
- `Http::fake` für 200, 202, 409, 410, 404, 429, 5xx, Timeout und Streaming
- Ratenbegrenzer greift
- Log und Protokoll enthalten keinen Schlüssel
- Verwaltungsmaske nur für Administratoren
## Risiken
- Antworten von ITM fehlen (C4–C8): Werte messen (3.2) und Standardwerte konfigurierbar halten.
+70
View File
@@ -0,0 +1,70 @@
# AP4 – Indexierung und LV-Suche
| | |
|---|---|
| Status | grob geplant, Detaillierung bis KW 43 |
| Aufwand | 1,0 PW |
| Zeitraum | KW 44–46 |
| Meilenstein | M2 |
| Freigabe | offen |
## Ziel
Alle LV-Positionen und Vorbemerkungen eines Projekts sind über Stichwort und Bedeutung
durchsuchbar. Jeder sieht nur, was er sehen darf, und springt vom Treffer ins PDF. Das ist auch
die Grundlage der Vorprüfung.
## Umfang
**Enthalten:**
- Abschnitte bilden, Embeddings erzeugen
- Rohvektoren in MySQL, Index in Meilisearch, Neuaufbau aus MySQL
- Zentraler Suchdienst mit Pflichtfilter für Rechte und Suchreihenfolge
- Hybride Suche mit Reranking
- Oberfläche der LV-Suche
**Nicht enthalten:**
- Vorschläge zur Prüfung (AP6)
- Regelwerk (Phase 3)
## Voraussetzungen
- AP2 Teil 1 (Ausstattung, Quantisierung), AP3 (Clients), AP5 (LV-Elemente)
- A2 (weitere Vertragsunterlagen?), A4 (beauftragte Nachträge mit durchsuchen?), A10 (Suchreihenfolge)
## Entscheidungen (vorläufig)
| Nr. | Frage | Vorschlag |
|---|---|---|
| E4.1 | Einheit eines Abschnitts | Je Position: OZ, Kurztext und Langtext. Vorbemerkungen nach Absätzen. PDFs ohne GAEB in Abschnitten von etwa 500–1.000 Tokens |
| E4.2 | Kandidaten vor dem Reranking | 40, danach die besten 10 (nach AP2 Teil 2 anpassen) |
| E4.3 | Gewichtung Stichwort/Bedeutung | `semanticRatio` 0,6, einstellbar |
## Schritte (grob)
- [ ] **4.1** Abschnittsbildung aus `lv_elemente` und PDF-Texten
- [ ] **4.2** Embedding-Jobs:
- Stapel, Vorrang für Suchanfragen
- Rohvektor als float32-BLOB mit Modell, Dimension und Vorverarbeitungsstand
- [ ] **4.3** Meilisearch-Index:
- Filterfelder Mandant, Land, Projekt, Abschnitt, Vertrag, Auftragnehmer, LV, Art, vertraulich
- Embedder `userProvided`
- [ ] **4.4** Zentraler Suchdienst:
- Pflichtfilter aus den Rechten des Benutzers
- Suchreihenfolge Vertrag → Abschnitt → Projekt nach den Projekteinstellungen
- Reranking
- [ ] **4.5** Neuaufbau des Index aus MySQL (Artisan-Befehl)
- [ ] **4.6** Oberfläche LV-Suche:
- Bereich wählbar: Vertrag, PFA oder Projekt
- Treffer nach LV gruppiert, „Im PDF zeigen“
- [ ] **4.7** Pilot-PFA auf Staging indexieren (du); Kennzahlen zu Dauer und Kosten
## Abnahmekriterien
- Die Kriterien von M2 zur LV-Suche sind erfüllt.
- Ein Benutzer ohne Recht erhält keinen Treffer und keine Trefferzahl aus fremden Projekten oder
vertraulichen Dokumenten (Tests).
## Risiken
- Last auf der KI-API bei der Erstindexierung (C8): nachts in Stapeln laufen lassen.
+85
View File
@@ -0,0 +1,85 @@
# AP5 – GAEB-Import und PDF-Anzeige
| | |
|---|---|
| Status | detailliert, wartet auf Freigabe |
| Aufwand | 1,25 PW |
| Zeitraum | KW 41 (Test), KW 43–45 |
| Meilenstein | M1 (LV-Baum), M2 (PDF, Nachträge) |
| Freigabe | offen |
## Ziel
LVs und Nachtrags-LVs werden aus X86-Dateien eingelesen: Positionen, Vorbemerkungen und Gliederung.
Jede Position lässt sich im zugehörigen PDF an der richtigen Stelle anzeigen.
## Umfang
**Enthalten:**
- GAEB DA XML (X86) für Vertrags-LVs und Nachtrags-LVs
- LV-Baum, PDF-Anzeige mit Markierung, Zuordnung von OZ zur PDF-Seite
- Nachtrag ohne X86 über Texterkennung mit Bestätigung durch den Bearbeiter
**Nicht enthalten:**
- D86 (GAEB 90), nur falls A5 es erfordert
- Kalkulationen und Preisnachweise (Phase 2)
## Voraussetzungen
- AP1 Schritt 1.10 (Upload)
- A3 (Vorbemerkungen in X86?), A5 (Nachtrags-LV immer als X86?), A18 (anonymisierte Beispiele)
## Entscheidungen
| Nr. | Frage | Vorschlag | Entschieden |
|---|---|---|---|
| E5.1 | Parser: eigene Umsetzung oder Bibliothek? | Nach Schritt 5.1; voraussichtlich eigene Umsetzung mit `XMLReader` (wenige gepflegte PHP-Bibliotheken) | offen |
| E5.2 | PDF-Anzeige | pdf.js (`pdfjs-dist`) lokal über Vite gebündelt, kein CDN | offen |
| E5.3 | Seitenzuordnung | PDF-Text je Seite lokal mit poppler (`pdftotext`), OZ suchen; Sail-Image um poppler-utils ergänzen | offen |
## Schritte
- [ ] **5.1** GAEB-Test (KW 41):
- Öffentliche Beispieldateien suchen und deren Lizenz prüfen; dazu eine selbst erzeugte
X86-Datei nach DA XML 3.3 mit Titeln, Positionen und Vorbemerkungen
- Struktur auswerten, Bibliotheken prüfen
→ `docs/tests/gaeb-test.md` mit Vorschlag zu E5.1
- [ ] **5.2** Datenmodell `lv_elemente`:
- Felder: LV, übergeordnetes Element, Typ (Bereich, Titel, Position, Vorbemerkung, Hinweistext),
OZ, Kurztext, Langtext, Menge, Einheit, EP, GP, Sortierung, PDF-Seite
- Migration und Tests
- [ ] **5.3** Parser und Import-Job X86 → `lv_elemente`:
- Importstatus und Fehlerbericht
- Erneuter Import ersetzt den alten Stand nachvollziehbar
- [ ] **5.4** LV-Ansicht:
- Baum mit Titeln, Positionen und Vorbemerkungen
- Langtext aufklappbar, Suche im LV
- [ ] **5.5** Seitenzuordnung (E5.3): je Element die PDF-Seite ermitteln, Abweichungen protokollieren
- [ ] **5.6** PDF-Anzeige (E5.2):
- Seite, Zoom, Sprung zur Position, Markierung der Stelle
- Auslieferung nur über die Rechteprüfung
- [ ] **5.7** Nachtrags-Import:
- Nachtrags-LV als X86 → `nachtragspositionen`
- Nur PDF → Dokumentdienst (AP3) → erkannte Positionen; der Bearbeiter bestätigt sie
- [ ] **5.8** Anpassen nach den echten Beispielen (A18), sobald sie da sind
## Abnahmekriterien
- Ein Beispiel-LV wird vollständig eingelesen: Anzahl der Positionen und Summen stimmen mit der Datei überein.
- Ein Klick auf eine Position zeigt im PDF die richtige Seite mit Markierung.
- Ein Nachtrags-LV ergibt die Nachtragspositionen.
## Tests
- Parser mit erfundenen Beispieldateien: Gliederung, Langtexte, Sonderfälle wie fehlende Menge
oder Pauschalposition
- Import-Job mit Fehlerfall
- Seitenzuordnung
- Rechteprüfung der PDF-Auslieferung
## Risiken
- Erweiterungen des Kalkulationsprogramms in der X86. Echte Beispiele früh anfordern (A18);
unbekannte Elemente protokollieren statt abbrechen.
- Gescannte PDFs ohne Textebene: dann keine Seitenzuordnung, Hinweis anzeigen.
+61
View File
@@ -0,0 +1,61 @@
# AP6 – Vorprüfung Stufe 1 und Prüfmaske
| | |
|---|---|
| Status | grob geplant, Detaillierung bis KW 45 |
| Aufwand | 1,25 PW |
| Zeitraum | KW 46–47 |
| Meilenstein | M2 |
| Freigabe | offen |
## Ziel
Für jede Position eines neuen Nachtrags stehen nach kurzer Zeit die wahrscheinlichsten Fundstellen
im Vertrag fest, noch ohne Sprachmodell. Der Bearbeiter prüft sie in der Prüfmaske, korrigiert sie
und setzt das Prüfergebnis.
## Umfang
**Enthalten:**
- Datenmodell für Prüfung, Fundstellen und Feedback
- Vorprüfungs-Job je Nachtrag, KI-Sicherheit aus Signalen
- Nachtragsliste, Prüfmaske v4 (ohne Sprachmodell-Teile)
- Feedback je Aspekt, Korrektur der Fundstelle, Prüfergebnis, Verlauf
**Nicht enthalten:**
- Einschätzung, Begründung und Bezugsposition durch das Sprachmodell (AP7)
- Regeln (AP8)
## Voraussetzungen
- AP4, AP5 (Nachtrags-Import), AP13
- Prüfmaske v4 von Claude Design (Stand 0.3, liegt vor)
- A9 (Werte des Prüfergebnisses)
## Schritte (grob)
- [ ] **6.1** Datenmodell:
- `pruefungen` je Nachtragsposition: Status, KI-Sicherheit, Prüfergebnis, Kommentar, bearbeitet von
- `fundstellen`: LV-Element, Rang, Wert, Suchbereich
- `feedback`: Aspekt, Antwort, Korrektur. Laut Prototyp 0.3: Fundstelle als Auswahl mit
„Auswahl bestätigen“ (gewählt: Kandidat, „keine“ oder „suche“); Prüfergebnis und Begründung
mit Ja / Nein. Jede Antwort wird sofort als Entwurf gespeichert. Ändert sich die bestätigte
Fundstelle, werden Prüfergebnis und Begründung neu angefordert und ihre Antworten
zurückgesetzt.
- [ ] **6.2** Vorprüfungs-Job: alle Positionen suchen (AP4), reranken, Fundstellen speichern,
Nachtrag auf „vorgeprüft“ setzen, Benachrichtigung
- [ ] **6.3** KI-Sicherheit aus Signalen: Wert der besten Fundstelle, Abstand zur zweiten, Art der
Fundstelle; „Warum?“-Erklärung
- [ ] **6.4** Nachtragsliste im Projekt und Positionsspalte mit Filter
- [ ] **6.5** Prüfmaske v4 mit drei Spalten:
- Fundstellen, Prüfergebnis, Begründung (Platzhalter bis AP7)
- Original-Nachtrag, Anschreiben und Quellen als Reiter
- [ ] **6.6** Feedback und Korrektur: richtige Fundstelle wählen, selbst suchen oder „keine
Fundstelle“; Prüfergebnis; Verlauf
- [ ] **6.7** Erste Messung: richtige Fundstelle unter den ersten drei (synthetische Fälle, auf
Staging mit echten Fällen)
## Abnahmekriterien
- Die Kriterien von M2 zur Vorprüfung sind erfüllt.
- „Speichern, weiter“ funktioniert erst, wenn alle drei Aspekte bewertet sind.
+54
View File
@@ -0,0 +1,54 @@
# AP7 – Vorprüfung Stufe 2 mit Sprachmodell
| | |
|---|---|
| Status | grob geplant, Detaillierung bis KW 47 |
| Aufwand | 1,0 PW |
| Zeitraum | KW 48–49 |
| Meilenstein | M3 |
| Freigabe | offen |
## Ziel
Das Sprachmodell beurteilt je Nachtragsposition, ob die Leistung im Vertrag enthalten ist. Bei
geänderter Leistung benennt es die Bezugsposition und den Unterschied. Die Begründung verweist nur
auf Quellen, die es wirklich gibt.
## Umfang
**Enthalten:**
- Prompt-Konzept, Kontextaufbau, strukturierte Antwort mit Prüfung im Code
- Bezugsposition mit Textvergleich, Begründung mit Quellen
- Verbrauchs- und Kostenprotokoll, Bewertung mit Testfällen
**Nicht enthalten:**
- Rechtliche Einordnung (A12)
- Preisbewertung (Phase 2)
## Voraussetzungen
- AP6
- C6 (Modell, Kontext, strukturierte Ausgabe)
- A6 (Bezugsposition im Nachtrag genannt?), A12
## Schritte (grob)
- [ ] **7.1** Prompt-Konzept `docs/prompts.md`:
- Rolle und Aufgabe, Ausgabe-Schema
- Regel „nur aus den gelieferten Quellen“
- Dokumentinhalte als Daten, nicht als Anweisungen
- [ ] **7.2** Kontextaufbau: Position, beste Fundstellen, passende Vorbemerkungen, später Regeln
und Referenzfälle; Grenze der Kontextlänge
- [ ] **7.3** Aufruf und Prüfung der Antwort:
- JSON-Schema, gültige Werte, zitierte Quellen müssen existieren
- eine Wiederholung bei ungültiger Antwort, sonst Rückfall auf Stufe 1
- [ ] **7.4** Bezugsposition und Unterschied: Textvergleich im Code, Anzeige als Abweichung
- [ ] **7.5** Anzeige in der Prüfmaske:
- Einschätzung „Im Vertrag“ und Vorschlag zum Prüfergebnis
- Begründung mit anklickbaren Quellen
- [ ] **7.6** Bewertung: synthetische Testfälle lokal; echte Fälle als Kennzahlen auf Staging
## Abnahmekriterien
- Die Kriterien von M3 zur Prüfmaske sind erfüllt.
- Keine Begründung verweist auf eine Quelle, die es nicht gibt (Test).
+46
View File
@@ -0,0 +1,46 @@
# AP8 – Regeln, Referenzfälle, Freigaben
| | |
|---|---|
| Status | grob geplant, Detaillierung bis KW 48 |
| Aufwand | 0,75 PW |
| Zeitraum | KW 49–50 |
| Meilenstein | M3 |
| Freigabe | offen |
## Ziel
Aus Korrekturen werden Regeln. Sie verbessern künftige Vorschläge im passenden Geltungsbereich und
werden je nach Einstellung freigegeben. Bestätigte Fälle dienen als Referenzfälle.
## Umfang
**Enthalten:**
- Regeln mit Situation, Regel, Geltungsbereich, Status und Versionen
- Regeldialog, Freigabe-Einstellungen (Voreinstellungen), Wissensbasis-Reiter
- Referenzfälle; Kennzeichnung „zur Überprüfung“ bei mehrfacher Überstimmung
**Nicht enthalten:** Training von Modellen.
## Voraussetzungen
- AP6, AP7
- A23 (projektübergreifende Regeln?), A24 (Freigabe-Variante)
- Prototyp 0.3 (Regeldialog v3) weicht ab: Freigabe immer durch den Projektleiter, keine
Freigabe-Einstellung; Geltungsbereich nur Vertrag, PFA, Projekt. Unser Vorschlag an BauIn (A24)
war „ohne Freigabe“, später umstellbar. **E8.1 offen:** Prototyp übernehmen oder Einstellung
behalten?
## Schritte (grob)
- [ ] **8.1** Datenmodell Regeln und Versionen; Geltungsbereich Vertrag → PFA → Projekt
(Auftraggeber und Land erst nach A23); nie löschen
- [ ] **8.2** Regeldialog aus der Prüfmaske: Vorformulierung, ähnliche Regeln
- [ ] **8.3** Freigabe nach E8.1 und Warteschlange „Zur Freigabe“
- [ ] **8.4** Regeln und Referenzfälle in Suche und Prompt einbeziehen; die speziellere Regel gewinnt
- [ ] **8.5** Wissensbasis: Regeln, Zur Freigabe, Referenzfälle, Allgemeine Dokumente
## Abnahmekriterien
- Eine Regel aus einer Korrektur wirkt nach der Freigabe beim nächsten passenden Fall und wird dort
als Quelle angezeigt.
+38
View File
@@ -0,0 +1,38 @@
# AP9 – Ergebnisliste und Export
| | |
|---|---|
| Status | grob geplant, Detaillierung bis KW 49 |
| Aufwand | 0,25 PW |
| Zeitraum | KW 50 |
| Meilenstein | M3 |
| Freigabe | offen |
## Ziel
Für jeden Nachtrag gibt es eine Ergebnisliste aller Positionen, die sich als Excel exportieren
und in die Bewertungsmatrix übertragen lässt.
## Umfang
**Enthalten:** Bearbeitungsstand des Nachtrags, Ergebnisliste, Excel-Export (PhpSpreadsheet),
Liste der Exporte, „Nachtrag abschließen“.
**Nicht enthalten:** Bewertungsmatrix in der DB-Vorlage (Option OP1) und Stellungnahme BÜW
(Phase 2, A15). In der Ergebnisliste erscheint deshalb nur der Button für die Bewertungsmatrix,
deaktiviert mit „Vorlage folgt“.
## Voraussetzungen
AP6, AP7; A9 (Werte), A14 (Spalten der Matrix als Orientierung).
## Schritte (grob)
- [ ] **9.1** Bearbeitungsstand eingegangen → vorgeprüft → in Prüfung → geprüft → exportiert
- [ ] **9.2** Ergebnisliste: OZ, Kurztext, Menge/Einheit, Im Vertrag, beste Fundstelle,
Prüfergebnis, Kommentar
- [ ] **9.3** Excel-Export und Exportprotokoll
## Abnahmekriterien
- Der Export eines Nachtrags enthält alle Positionen mit Prüfergebnis und Fundstelle.
@@ -0,0 +1,38 @@
# AP10 – Dashboard, Benachrichtigungen, Auswertungen
| | |
|---|---|
| Status | grob geplant, Detaillierung bis KW 46 |
| Aufwand | 0,5 PW |
| Zeitraum | KW 47 (Dashboard), KW 2/2027 (Auswertungen) |
| Meilenstein | M2, M4 |
| Freigabe | offen |
## Ziel
Jeder sieht beim Einstieg seine offenen Nachträge und wird benachrichtigt, wenn etwas fertig ist.
Die Auswertungen zeigen, wie gut die Vorschläge sind.
## Umfang
**Enthalten:**
- Dashboard nach Prototyp „Dashboard v3“ (2a, 2b Benachrichtigungen, 2c leer, 2e Hilfe)
- Benachrichtigungen nur in der Anwendung (Glocke in der Topbar); keine E-Mails (B3)
- Auswertungen: Trefferquote, Treffsicherheit je KI-Sicherheit, überstimmte Regeln, Prüfzeit
- Allgemeine Dokumente in den Einstellungen
## Voraussetzungen
AP6. B3 ist beantwortet: keine E-Mails.
## Schritte (grob)
- [ ] **10.1** Dashboard: offene Nachträge, „vorgeprüft – bereit“, Verarbeitung, Freigaben
- [ ] **10.2** Benachrichtigungen: Vorprüfung fertig oder fehlgeschlagen, Nachtrag zugewiesen,
Regel zur Freigabe
- [ ] **10.3** Auswertungen (KW 2)
- [ ] **10.4** Allgemeine Dokumente (Einstellungen, projektübergreifend)
## Abnahmekriterien
- Die Kennzahlen der Auswertungen stimmen mit einer Nachzählung auf Testdaten überein.
+95
View File
@@ -0,0 +1,95 @@
# AP11 – Betrieb
| | |
|---|---|
| Status | in Arbeit (11a.1 erledigt); Schritte ab 11a.2 detailliert, warten auf Freigabe |
| Aufwand | 0,5 PW (wir) + 1–1,5 PW (ITM-Entwickler) |
| Zeitraum | KW 41–44 (Staging), KW 50–51 (Produktion), KW 3/2027 (Restore-Test) |
| Meilenstein | M2 (Staging), M3 (Produktion), M4 (abgesichert) |
| Freigabe | offen |
## Ziel
Staging und Produktion laufen auf den Servern von ITM, werden reproduzierbar bereitgestellt,
gesichert und überwacht. Ein Ausfall fällt auf, bevor BauIn ihn bemerkt.
## Umfang
**Enthalten:**
- **11a, wir:**
- Server-Anforderungen
- Vorlagen für Nginx, systemd und Cron
- Deploy-Skript
- Lebenszeichen- und Prüfbefehle
- Backup-Skript
- Betriebsdoku
- **11b, ITM-Entwickler:**
- Server aufsetzen, Domain und Zertifikat
- Backup-Ziel, Überwachung
- Restore-Test gemeinsam mit uns
**Nicht enthalten:** Hochverfügbarkeit, mehrere Server.
## Voraussetzungen
- C10 (05.10.): Ausstattung, Zugang, Verschlüsselung und Sicherung der Server sind Sache von ITM.
Wir liefern die Server-Anforderungen und die Vorlagen (11a).
- B3 (05.10.): Die Anwendung verschickt keine E-Mails. Die Adresse wird vor dem Livegang geplant.
- C12 ist beantwortet, offen blieb: Wer deployt im Betrieb, und wie viel Zeit hat der
ITM-Entwickler?
- B2, B5 und B6 werden vor dem Livegang geklärt (Abschnitt unten).
## Vor dem Livegang zu klären
Für die Entwicklung des Prototyps nicht nötig, vor dem Livegang aber Pflicht. Stand 05.10.:
| Thema | Frage | Mit wem |
|---|---|---|
| Zugriff (B2) | Aus dem Internet mit Login, oder nur über VPN bzw. feste IP-Adressen? | BauIn IT, ITM |
| Adresse (B3) | Domain und Zertifikat | BauIn IT, ITM |
| 2FA (B1) | Pflicht einschalten (Schalter aus AP1, E1.6) und Benutzer vorher informieren | BauIn |
| Backups (B5) | Speicherort, Aufbewahrung, Zuständigkeit | BauIn IT, ITM |
| Datenschutz und Informationssicherheit (B6) | Vorgaben von BauIn oder der DB, z. B. Sicherheitsanforderungen an Auftragnehmer der DB, ISO 27001, Geheimhaltung. Wer muss der Verarbeitung über die KI-API zustimmen? | BauIn IT |
| Datenverarbeitung bei API-Werk (C2) | Standort, Speicherdauer, Auftragsverarbeitung, kein Training. Klärt ITM; wir brauchen nur das Ergebnis für die Doku | ITM |
| Alarmierung (E11.3) | Ohne E-Mail aus der Anwendung: über die Überwachung von ITM? | ITM |
Nach dem Livegang von Phase 1: Aufbewahrung nach Projektende (A27), Nutzung der DB-Richtlinien in
einem KI-System (A28).
## Entscheidungen
| Nr. | Frage | Vorschlag | Entschieden |
|---|---|---|---|
| E11.1 | Deployment | Skript auf dem Server mit Versionsverzeichnissen (`releases/`, `current`), ausgelöst per SSH | offen |
| E11.2 | Backup-Werkzeug | restic, verschlüsselt, Ziel von ITM | offen (B5) |
| E11.3 | Alarmierung | Die Anwendung verschickt keine E-Mails (B3). Vorschlag: Lebenszeichen und Prüfbefehle, die die Überwachung von ITM abfragt | offen (mit ITM) |
## Schritte
- [x] **11a.1** Server-Anforderungen `docs/server-anforderungen.md`
- [ ] **11a.2** Vorlagen in `deploy/`:
- Nginx-Server-Block, systemd-Units (Worker, Meilisearch), Cron-Zeile
- Beispiel für `.env` ohne Werte
- [ ] **11a.3** Deploy-Skript nach E11.1 mit Prüfungen (Migration, Caches, Neustart der Worker),
zuerst gegen eine lokale Test-VM oder direkt auf Staging
- [ ] **11a.4** Betriebsbefehle:
- Lebenszeichen für Worker und Scheduler
- Zählung fehlgeschlagener Jobs
- Statusprüfung der KI-API (aus AP3)
- Meldung bei Problemen nach E11.3
- [ ] **11b.1** Staging aufsetzen (ITM-Entwickler, KW 42–44), erstes Deployment gemeinsam
- [ ] **11a.5** Backup-Skript nach E11.2 und Betriebsdoku `docs/betrieb.md`:
- Deployment, Wiederherstellung, `APP_KEY` sicher aufbewahren
- [ ] **11b.2** Produktion aufsetzen (KW 50–51)
- [ ] **11a.6 / 11b.3** Restore-Test auf einem Ersatzsystem (KW 3), Protokoll in `docs/betrieb.md`
## Abnahmekriterien
- Ein Deployment aus dem Gitea auf Staging klappt mit einem Befehl.
- Ein absichtlich gestoppter Worker löst innerhalb von 10 Minuten einen Alarm aus (Weg nach E11.3).
- Der Restore-Test war erfolgreich und ist dokumentiert.
## Risiken
- Antworten von ITM zu Zugang und Ausstattung kommen spät. Dann Staging per Bildschirmfreigabe
gemeinsam aufsetzen.
+40
View File
@@ -0,0 +1,40 @@
# AP12 – Pilot und Messung
| | |
|---|---|
| Status | grob geplant, Detaillierung bis KW 51 |
| Aufwand | 1,5 PW |
| Zeitraum | KW 1–2/2027 |
| Meilenstein | M4 |
| Freigabe | offen |
## Ziel
Alle Nachträge des Pilot-PFA laufen durch das System. Trefferquote und Zeitersparnis sind
gemessen, und die Vorschläge sind anhand der Ergebnisse nachgeschärft.
## Umfang
**Enthalten:**
- Messplan, Messskripte (nur Kennzahlen), Durchlauf mit BauIn
- Nachschärfen: Prompts, Gewichtung, Quantisierung, Kandidatenzahl
- Abschlussbericht
## Voraussetzungen
AP6–AP9; A20 (Referenzfälle), A21 (Erfolgskriterium); Fachexperte verfügbar.
## Schritte (grob)
- [ ] **12.1** Messplan (KW 51):
- Kennzahlen: richtige Fundstelle unter den ersten drei, Übereinstimmung des Prüfergebnisses,
Prüfzeit je Nachtrag
- Vergleich mit dem Stand vorher
- [ ] **12.2** Messskripte für Staging bzw. Produktion, ohne Inhalte
- [ ] **12.3** Durchlauf (KW 1) mit Bewertung durch den Fachexperten
- [ ] **12.4** Nachschärfen (KW 2) und erneute Messung
- [ ] **12.5** Abschlussbericht `docs/pilot-bericht.md` für M4
## Abnahmekriterien
- Die Kriterien von M4 zu den Kennzahlen sind erfüllt bzw. dokumentiert.
+121
View File
@@ -0,0 +1,121 @@
# AP13 – Designsystem aus Claude Design
| | |
|---|---|
| Status | detailliert, wartet auf Freigabe |
| Aufwand | 0,5 PW |
| Zeitraum | KW 43 |
| Meilenstein | M1 |
| Freigabe | offen |
## Ziel
Die Anwendung sieht aus wie der Prototyp von Claude Design: Farben, Schriften, Abstände,
Statussysteme, App-Rahmen. Bestehende und künftige Seiten nutzen dieselben Bausteine.
## Umfang
**Enthalten:**
- Design-Tokens mit Hell- und Dunkelmodus
- Schriften und Icons lokal
- TallStackUI anpassen
- Eigene Komponenten für die Statussysteme
- Sidebar und Topbar
- Musterseite zum Abgleich
**Nicht enthalten:** Einzelne Fachseiten; diese entstehen in ihren Arbeitspaketen mit den hier
gebauten Bausteinen.
## Voraussetzungen
Erfüllt am 05.10.: **Stand 0.3** von Claude Design mit unseren Änderungen 1–26 und einer
UX-Überarbeitung. Er liegt im Planungsordner unter `claude-design/2026-10-05_Stand-0.3/`:
- `Uebergabe.md`: maßgebliche Spezifikation, Abschnitte 0–9
- `README.md`: Zusammenfassung mit Design-Tokens
- `prototyp/`: `.dc.html`-Dateien mit `support.js`, im Browser zu öffnen
- `screenshots/`: 27 PNG, ein Bild je Zustand, 1440 px breit, für den Abgleich
- `icons/`: Phosphor 2.1.1 als SVG
- `logo/`: Logo-SVGs
Der Stand ist **verbindlich** (High-Fidelity): Farben, Schriften, Abstände, Texte und Zustände
werden genau übernommen. Bei Widerspruch gilt der Prototyp vor den Screenshots.
## Entscheidungen
| Nr. | Frage | Vorschlag | Entschieden |
|---|---|---|---|
| E13.1 | Schriften | League Spartan und IBM Plex über `@fontsource`-Pakete, lokal gebündelt. Sie sind nicht im Übergabepaket | offen |
| E13.2 | Icons | Die gelieferten SVGs (Phosphor 2.1.1, MIT) als lokales Icon-Set; TallStackUI erlaubt eigene SVG-Komponenten. Alternative: Blade-Paket `codeat3/blade-phosphor-icons`, dann Version gegen 2.1.1 prüfen | offen |
| E13.3 | Ablage der Entwürfe im Repository | Stand 0.3 vollständig nach `docs/design/` (nur erfundene Daten, rund 5 MB), damit Claude Code bei jeder Seite nachsehen kann | offen |
## Schritte
- [ ] **13.1** Stand 0.3 in `docs/design/` ablegen (E13.3). Unterschiede zur Rückmeldung und zu
den Antworten sind schon notiert (Abschnitt „Abgleich Stand 0.3“)
- [ ] **13.2** Design-Tokens als CSS-Variablen (hell und dunkel) und Tailwind-Theme
- [ ] **13.3** Schriften (E13.1) und Icons (E13.2) einbinden; `NoExternalResourcesTest` bleibt grün
- [ ] **13.4** TallStackUI anpassen: Buttons, Eingabefelder, Select, Karte, Dialog, Tabelle,
Toast, Badge (Radien, Farben, Größen)
- [ ] **13.5** Eigene Komponenten:
- KI-Sicherheit (`x-confidence`)
- Prüfergebnis-Plakette
- Regelstatus-Etikett
- Bearbeitungsstand mit 5 Punkten
- KI-Vorschlag-Pille
- Zustände leer, lädt, Fehler, kein Zugriff
- [ ] **13.6** App-Rahmen: Sidebar mit Gruppen Arbeit, Wissen, System; Topbar mit Suche,
Benachrichtigungen, Benutzer; Länderwahl nur bei mehreren Ländern
- [ ] **13.7** Musterseite „Designsystem“ (nur in der Entwicklung) und Abgleich mit dem Prototyp
per Screenshot; bestehende Seiten (Login, Einstellungen, AP1-Seiten) umstellen
## Abgleich Stand 0.3 (05.10.)
**Passt zu Plan und Entscheidungen:**
- TallStackUI 4 mit Präfix `ts-`, eigene Komponenten ohne Präfix (`x-confidence`,
`x-review-question`, `x-source-choice`, `x-contract-coverage`, `x-coachmark`)
- Schriften und Icons in der Anwendung lokal; der Prototyp lädt sie nur zur Ansicht von fremden Servern
- Gliederung Projekt → PFA → Vertrag → LVs, Nachträge am Vertrag, MKA nur als Verweis (A8)
- Nur „dem Grunde nach“: keine Mengen, Preise oder Anspruchsgrundlage. Höhe und Regelwerk sind
nicht entworfen; die Quellenanzeige in Spalte 3 ist für den späteren Chat wiederverwendbar.
- Suchbereiche: Vertrag, andere Verträge im PFA per Schalter, andere PFA nur als Hinweis mit EP,
nie projektübergreifend (A10, A11)
- Rollen Administrator, Projektleiter, Bearbeiter, Leser. „Kein Zugriff“ verrät nicht, ob es das
Objekt gibt (A25, AP1).
- Länderwahl nur bei mehreren Ländern (A26: Österreich in Phase 2)
- Benachrichtigungen nur in der Anwendung (B3)
- Ergebnisliste mit Excel-Export; Bewertungsmatrix deaktiviert, bis die Vorlage da ist (OP1)
**Abweichungen und wie wir sie behandeln** (Vorschlag, bitte bestätigen):
| Stelle im Prototyp | Widerspricht | Umgang |
|---|---|---|
| Projekt anlegen v3 · 2b „Ablage für Dateien oder ZIP mit Ordnern“; Übergabe 6.8 „Upload allgemein … ZIP“ | B4: kein ZIP | Nur Mehrfach-Upload, Text ohne „ZIP“ |
| Ergebnisliste v2: Export „Stellungnahme (Word)“, deaktiviert; Übergabe 6.8: Export-Vorlage Stellungnahme | A15: Phase 2 | Weglassen |
| Regeldialog v3: „Freigabe immer durch den Projektleiter“, keine Einstellung | A24-Vorschlag „ohne Freigabe“, Gesamtplan „einstellbare Freigaben“ | Entscheidung E8.1 in AP8 |
| Projektdetail v2: Reiter nur PFA & Verträge · Nachträge · Berechtigungen · Einstellungen; Übersicht, Dokumente und Protokoll aus Rückmeldung 19 fehlen | – | Prototyp gilt; Protokoll unter Verwaltung (AP1, 1.11). Falls A2 weitere Vertragsunterlagen bringt, braucht es dafür einen Ort |
| Projektdetail v2 · Einstellungen: nur „Andere Verträge im selben PFA einbeziehen“; „Andere PFAs“ und „Nur gleicher Auftragnehmer“ entfallen | A10 noch offen | Prototyp gilt, bis A10 etwas anderes ergibt; Datenmodell in AP1, 1.4b |
| Projekt anlegen v3 ohne Vertragsgrundlage | `projekte.vertragsgrundlage` im Datenmodell | Feld bleibt mit Standardwert, nicht im Formular (AP1, 1.4b) |
| Zielgruppe „nur deutschsprachig“ | Oberfläche Deutsch und Englisch | Kein Widerspruch: Die deutschen Texte werden wörtlich übernommen, Englisch bleibt als Übersetzung |
| Logo: Wortmarke als Text, „ein finales Logo gibt es noch nicht“ | – | Vorläufig verwenden, vor dem Einsatz in Pfade umwandeln |
| „Passt / Passt nicht“ aus unserer Rückmeldung (7, 9, 11) | – | In 0.3 ersetzt durch Auswahl der Fundstelle und Ja / Nein; AP6 angepasst |
Nicht im Paket und auch nicht nötig: die Archivordner „Stand 0.1/0.2“ und die Schriftdateien.
Wenn die Abweichungen bestätigt sind, geht eine kurze Rückmeldung im Format von Abschnitt 9 an
Claude Design (ZIP und Stellungnahme entfernen, E8.1).
## Abnahmekriterien
- Die Musterseite entspricht dem Designsystem des Prototyps in Hell und Dunkel.
- Alle Statussysteme sind ohne Farbe unterscheidbar (Symbol und Text).
- Es werden keine externen Ressourcen geladen.
## Tests
- `NoExternalResourcesTest` auf allen Seiten
- Blade-Komponenten-Tests für Status und Zustände
- Übersetzungsabdeckung
## Risiken
- Weitere Stände von Claude Design während der Umsetzung: Nur übernehmen, was im
Änderungsprotokoll der Übergabe (Abschnitt 8) steht, und Tokens zentral halten.
+33
View File
@@ -0,0 +1,33 @@
# Optionen OP1 und OP2
| | |
|---|---|
| Status | grob geplant; Umsetzung nur nach Beauftragung und mit Vorlage |
| Aufwand | je 0,5 PW |
| Zeitraum | OP1 KW 51 (bei 35–40 h pro Woche), sonst nach M4. OP2 erst in Phase 2 (A15, 05.10.) |
| Freigabe | offen |
## OP1 – Bewertungsmatrix in der Excel-Vorlage der DB
**Ziel:** Die Ergebnisse eines Nachtrags werden direkt in die Bewertungsmatrix der DB geschrieben.
**Voraussetzungen:** A14 (Vorlage leer und ausgefüllt, Spalten und Reiter, Makros?).
**Schritte (grob):**
- [ ] **OP1.1** Vorlage untersuchen. Bleiben die Makros erhalten, wenn PhpSpreadsheet die Datei
schreibt? Falls nicht: Werte so exportieren, dass sie sich in die Vorlage einfügen lassen.
- [ ] **OP1.2** Zuordnung Spalten ↔ Daten mit dir und BauIn abstimmen
- [ ] **OP1.3** Export und Test mit einem Beispielnachtrag
## OP2 – Stellungnahme BÜW als Word-Datei
**Vertagt auf Phase 2** (A15, 05.10.). Die Schritte bleiben als Merkposten stehen.
**Ziel:** Das Deckblatt zur Bewertungsmatrix (Ansprechpartner, Bearbeiter, Nachtrag, Sachverhalt)
wird aus der Vorlage erzeugt.
**Voraussetzungen:** A15 (Vorlage, `.docx`, variable Felder).
**Schritte (grob):**
- [ ] **OP2.1** Vorlage in `.docx` mit Platzhaltern (einmalige Umwandlung)
- [ ] **OP2.2** Felder befüllen (PHPWord-TemplateProcessor), Test
+47
View File
@@ -0,0 +1,47 @@
# APx – Titel
| | |
|---|---|
| Status | grob geplant / detailliert (wartet auf Freigabe) / freigegeben / in Arbeit / erledigt |
| Aufwand | x PW (mit Claude Code) |
| Zeitraum | KW … |
| Meilenstein | M… |
| Freigabe | Datum, durch wen |
## Ziel
Ein bis drei Sätze: Was kann man danach, das vorher nicht ging?
## Umfang
**Enthalten:** …
**Nicht enthalten:** …
## Voraussetzungen
Andere Arbeitspakete, Zulieferungen, Fragen aus `docs/fragen.md` (mit ID).
## Entscheidungen
| Nr. | Frage | Vorschlag | Entschieden |
|---|---|---|---|
| Ex.1 | … | … | offen |
## Schritte
Je Schritt höchstens ein Tag, mit prüfbarem Ergebnis.
- [ ] **x.1** … → Ergebnis: …
## Abnahmekriterien
- …
## Tests
- …
## Risiken
- …
+296
View File
@@ -0,0 +1,296 @@
# KI-BauIN – Projektplan Phase 1
Stand: 5. Oktober 2026 (KW 41). Dieses Dokument ist der Gesamtplan. Die Einzelheiten stehen in
den Teilplänen unter `docs/plaene/`, offene Fragen mit Status in `docs/fragen.md`.
## Inhalt
1. [Arbeitsweise](#1-arbeitsweise)
2. [Beteiligte und Zuständigkeiten](#2-beteiligte-und-zuständigkeiten)
3. [Ziel und Umfang](#3-ziel-und-umfang)
4. [Rahmen und Annahmen](#4-rahmen-und-annahmen)
5. [Meilensteine mit Abnahmekriterien](#5-meilensteine-mit-abnahmekriterien)
6. [Arbeitspakete](#6-arbeitspakete)
7. [Zeitplan nach Kalenderwochen](#7-zeitplan-nach-kalenderwochen)
8. [Zulieferungen](#8-zulieferungen)
9. [Risiken](#9-risiken)
10. [Entscheidungen](#10-entscheidungen)
11. [Nach Phase 1](#11-nach-phase-1)
12. [Änderungen an diesem Plan](#12-änderungen-an-diesem-plan)
## 1. Arbeitsweise
Es wird nur umgesetzt, was in einem freigegebenen Teilplan steht.
1. **Teilplan detaillieren:** Vor Beginn eines Arbeitspakets beschreibt Claude Code den Teilplan
in `docs/plaene/`. Er enthält Ziel, Umfang, Schritte (je höchstens ein Tag), offene Fragen,
Entscheidungen und Abnahmekriterien.
2. **Freigabe:** Du liest den Teilplan, triffst die offenen Entscheidungen und setzt den Status
auf „freigegeben“.
3. **Umsetzung in Schritten:** Jeder Schritt endet mit grünen Tests, Pint und PHPStan und einem
Commit, der den Schritt nennt (z. B. „AP1 Schritt 1.7: …“). Änderungen an Rechten,
Dateizugriff, Suchfiltern oder KI-Aufrufen nennt Claude Code ausdrücklich.
4. **Abnahme:** Am Ende eines Arbeitspakets prüfst du die Abnahmekriterien im Browser. Danach
steht der Status auf „erledigt“, und Plan und Fragenliste werden nachgezogen.
5. **Neue Erkenntnisse zuerst in den Plan:** Antworten, neue Wünsche und Probleme kommen zuerst
in `docs/fragen.md` oder den Teilplan, dann in den Code. Ideen außerhalb des Plans kommen in
den Abschnitt [Nach Phase 1](#11-nach-phase-1).
6. **Rollierend:** Die nächsten zwei bis drei Wochen sind im Detail geplant, alles Weitere grob.
Ein Teilplan wird spätestens eine Woche vor seinem Start detailliert.
7. **Rhythmus:** Vor jedem Freitagstermin mit BauIn gibt es einen Statusbericht mit Screenshots.
Danach werden Antworten und Termine eingepflegt.
**Status eines Teilplans:** grob geplant → detailliert (wartet auf Freigabe) → freigegeben →
in Arbeit → erledigt.
**Definition of Done, für jeden Schritt:**
- Tests sind vorhanden und grün, bei geschützten Aktionen auch ohne Berechtigung.
- Pint und PHPStan Stufe 7 laufen ohne Fehler.
- Sichtbare Texte stehen in `__()` und sind übersetzt.
- Keine externen Ressourcen, keine Geheimnisse, keine echten Kundendaten.
- Doku und Teilplan sind aktualisiert, der Commit ist auf Deutsch.
## 2. Beteiligte und Zuständigkeiten
| Wer | Aufgabe im Projekt |
|---|---|
| **Du** (Entwicklung, Steuerung) | Planung freigeben, entscheiden, im Browser testen, mit BauIn und ITM sprechen, echte Daten auf Staging testen, pushen. Du schreibst selbst keinen Code. |
| **Claude Code** | Teilpläne ausarbeiten, den gesamten Code mit Tests schreiben, Doku, Statusberichte entwerfen. Sieht keine echten Kundendaten und keine API-Schlüssel. |
| **ITM-Entwickler** | KI-API (API-Werk, Modelle, Schlüssel, Grenzen), Aufbau von Staging- und Produktionsserver nach `docs/server-anforderungen.md` |
| **ITM-Projektleitung** | Projektleitung, Abrechnung, Kostenrahmen für BauIn, Server und Gitea. Allein Sache von ITM: Preise der KI-API, Datenverarbeitung bei API-Werk, Betrieb und Sicherung der Server (C2, C3, C10) |
| **Claude Design** | Oberflächenentwürfe (Prototyp, Designsystem); Übergaben im Format seiner Übergabe, Abschnitt 9 |
| **BauIn – Nachtragsbearbeitung** | Fachliche Fragen, Testdaten, Referenzfälle, Vorlagen, Bewertung im Pilot |
| **BauIn – IT** | Anmeldung, Zugriff, Domain, Datenschutz und Informationssicherheit |
BauIn ist der Endkunde und prüft als Bauüberwachung im Auftrag der DB die Nachträge der
Auftragnehmer dem Grunde nach. Kontakt läuft über Teams, Termine sind freitags.
## 3. Ziel und Umfang
**Ziel von Phase 1:** Für jede Position eines eingereichten Nachtrags zeigt KI-BauIN, ob die
Leistung schon im Vertrag enthalten ist, also in den LVs einschließlich Vorbemerkungen. Dazu
gehören Fundstellen mit Sprung ins PDF, eine Begründung und bei geänderter Leistung die
Bezugsposition. Der Bearbeiter entscheidet; seine Rückmeldungen verbessern die Vorschläge.
**Enthalten:**
- Projekte mit PFA (Abschnitten), Verträgen und Auftragnehmern
- Upload von LVs als X86 und PDF (mehrere Dateien auf einmal, kein ZIP), Import der Positionen
und Vorbemerkungen
- LV-Suche über Vertrag, PFA oder Projekt mit Sprung ins PDF
- Nachträge anlegen (Nachtrags-LV, Anschreiben, Anlagen) und Vorprüfung aller Positionen
- Prüfmaske mit Fundstellen, Bezugsposition, Einschätzung, Begründung, KI-Sicherheit, Feedback
- Regeln, Referenzfälle, einstellbare Freigaben
- Ergebnisliste mit Excel-Export
- Rollen und Rechte, vertrauliche Dokumente, Protokoll, Deutsch und Englisch
- Betrieb auf den Servern von ITM: Staging und Produktion, Backups, Alarmierung
**Optional, wenn Zeit und Vorlage da sind:** Bewertungsmatrix in der Excel-Vorlage der DB (OP1).
**Nicht enthalten** (macht BauIn selbst oder kommt später):
- rechtliche Einordnung, Fristen, § 2 Abs. 8, § 6 Abs. 6, Anordnungen (A13)
- Prüfung der Höhe nach, Stellungnahme BÜW als Word-Datei (A15), Österreich (A26): Phase 2
- Regelwerk mit Chat (Phase 3)
- DOXIS-Anbindung (A16: Upload bleibt Handarbeit)
- E-Mails jeder Art (B3), Anmeldung über das Microsoft-Konto (B1)
## 4. Rahmen und Annahmen
- **Gliederung:** Projekt → PFA (Planfeststellungsabschnitt) → Vertrag mit einem Auftragnehmer →
viele LVs; Nachträge gehören zu einem Vertrag. Datenmodell in `docs/datenmodell.md`.
- **Daten:** LVs liegen immer als X86, D86 und PDF vor. Grundlage ist die X86-Datei, das PDF dient
der Anzeige. Texterkennung über die KI-API braucht es nur für PDFs ohne GAEB-Datei.
- **KI-API ITM** über API-Werk: Dokumentdienst, `embed`, `rerank`, `chat`, `chat-noreasoning`.
Es gibt keine Webhooks, Ergebnisse werden abgefragt. Grenzen: 120 Anfragen/min je Schlüssel,
50 MB je Datei. Einzelheiten in `docs/tech-stack.md`.
- **Schlüssel:** In der Entwicklung über den Schlüssel-Proxy (`tools/ki-proxy`), in der Anwendung
verschlüsselt in der Datenbank, nie in `.env`. Es gibt nur Schlüssel für die Produktion (C1):
Entwicklung, Staging und Produktion teilen sich Zählung, Kosten und das Limit. Automatische
Tests rufen die echte API deshalb nie auf.
- **Anmeldung:** eigenes Passwort, 2FA zunächst aus; später wird sie eingeschaltet und ist dann
Pflicht (B1). Die Anwendung verschickt keine E-Mails (B3), Benachrichtigungen gibt es nur in der
Anwendung.
- **Echte Daten:** Entwickelt wird mit öffentlichen bzw. erfundenen GAEB-Dateien und einem
anonymisierten Beispielablauf von BauIn. Echte Pilotdaten dürfen auf den Servern von ITM liegen
und über die KI-API verarbeitet werden (A19). Sie gehen weiterhin nie an Claude Code.
- **Vor dem Livegang zu klären:** Zugriff, Adresse, Backups, Datenschutz und
Informationssicherheit, 2FA-Pflicht. Liste in AP11, Abschnitt „Vor dem Livegang zu klären“.
- **Aufwand** in deinen Personenwochen (PW, 40 h) mit Claude Code: Kern rund 11 PW, mit Puffer
13–14 PW, die Option OP1 0,5 PW. Ohne KI-Unterstützung wären es rund 23 PW. Beim
ITM-Entwickler kommen 1–1,5 PW für die Server dazu.
- **Verfügbarkeit:** rund 30 h pro Woche (≈ 0,75 PW). Bis M4 sind das ≈ 11,25 PW, genau der Kern.
Empfehlung: in KW 44–51 auf 35–40 h erhöhen, dann passen OP1 und etwas Reserve.
- **Pause:** KW 52–53 (21.12.2026 – 03.01.2027).
## 5. Meilensteine mit Abnahmekriterien
| | Termin | Abnahmekriterien |
|---|---|---|
| – | Fr 16.10. (KW 42) | Termin BauIn: Statusbericht, Plan, Kostenrahmen (ITM), ★-Fragen besprochen, Liefertermine vereinbart |
| **M1** | Fr 30.10. (KW 44) | Siehe unten |
| **M2** | Fr 20.11. (KW 47) | Siehe unten |
| **M3** | Fr 18.12. (KW 51) | Siehe unten |
| **M4** | Fr 29.01.2027 (KW 4) | Siehe unten |
| Puffer | KW 5–6/2027 | für Verzögerungen bei Zulieferungen oder Import |
**M1 – Grundgerüst** (Demo 30 min, lokal oder auf Staging):
- Ein Administrator legt Benutzer an; Anmeldung mit eigenem Passwort funktioniert, 2FA ist
einrichtbar und lässt sich als Pflicht einschalten (B1).
- Ein Projekt wird mit PFA, Verträgen und Mitgliedern angelegt.
- Mehrere LV-Dateien (X86 + PDF) werden auf einmal hochgeladen; die Paare werden erkannt, das LV
wird importiert und als Baum mit Vorbemerkungen angezeigt.
- Ein Benutzer ohne Projektrecht sieht nichts davon, auch nicht über eine direkte URL (Tests).
- Das Protokoll zeigt Uploads und Rechteänderungen; CI ist grün.
- Das Designsystem aus Claude Design (Stand 0.3) ist angewendet.
**M2 – LV-Suche und erste Vorprüfung** (Prüftermin 60 min, auf Staging):
- Staging bei ITM läuft, der Pilot-PFA ist importiert (erlaubt laut A19).
- Die LV-Suche findet über Vertrag, PFA oder Projekt und springt im PDF an die Stelle.
- Für die ersten Nachträge liefert die Vorprüfung ohne Sprachmodell Fundstellen je Position.
- Für mindestens 5 Nachträge ist gemeinsam mit BauIn erfasst, ob die richtige Fundstelle unter
den ersten drei ist.
**M3 – Nachträge durchgängig** (Präsentation 60–90 min):
- Prüfmaske mit Einschätzung „im Vertrag enthalten?“, Bezugsposition und Begründung mit
anklickbaren Quellen.
- Feedback und Korrektur, Regeln mit Freigabe, Ergebnisliste mit Excel-Export.
- Bewertungsmatrix (OP1), falls die Vorlage rechtzeitig da war.
- Produktionsserver steht.
**M4 – Pilot abgeschlossen** (Abschluss):
- Alle Nachträge des Pilot-PFA sind durchlaufen.
- Kennzahlen liegen vor: richtige Fundstelle unter den ersten drei in X % (Ziel aus Frage A21),
Prüfzeit je Nachtrag vorher und nachher.
- Produktion mit Backups, Alarmierung und erfolgreichem Restore-Test, Betriebsdoku.
- Entscheidung über Phase 2.
## 6. Arbeitspakete
PW = deine Personenwochen mit Claude Code. Status nach Abschnitt 1.
| AP | Teilplan | PW | Zeitraum | Status | Hängt ab von |
|---|---|---|---|---|---|
| AP0 | [Steuerung, Klärung, Doku](plaene/AP00-steuerung.md) | 0,75 | laufend | in Arbeit | – |
| AP1 | [Fundament: CI, Rechte, Benutzer, Projekte, Upload, Protokoll](plaene/AP01-fundament.md) | 1,25 | KW 40–43 | in Arbeit; Schritte ab 1.5 warten auf Freigabe | A1, A25 |
| AP2 | [Meilisearch-Test](plaene/AP02-meilisearch-test.md) | 0,25 | KW 41 / KW 46 | detailliert, wartet auf Freigabe | Teil 2: Staging, A20 |
| AP3 | [Anbindung KI-API ITM](plaene/AP03-ki-api.md) | 0,5 | KW 42–44 | in Arbeit; Schritte ab 3.2 warten auf Freigabe | C1, C4–C8 |
| AP4 | [Indexierung und LV-Suche](plaene/AP04-indexierung-suche.md) | 1,0 | KW 44–46 | grob geplant | AP2, AP3, AP5, A10 |
| AP5 | [GAEB-Import und PDF-Anzeige](plaene/AP05-gaeb-pdf.md) | 1,25 | KW 41 / KW 43–45 | detailliert, wartet auf Freigabe | A3, A5, A18 |
| AP6 | [Vorprüfung Stufe 1 und Prüfmaske](plaene/AP06-vorpruefung-stufe1.md) | 1,25 | KW 46–47 | grob geplant | AP4, AP5, AP13 |
| AP7 | [Vorprüfung Stufe 2 mit Sprachmodell](plaene/AP07-vorpruefung-stufe2.md) | 1,0 | KW 48–49 | grob geplant | AP6, C6 |
| AP8 | [Regeln, Referenzfälle, Freigaben](plaene/AP08-regeln.md) | 0,75 | KW 49–50 | grob geplant | A23, A24 |
| AP9 | [Ergebnisliste und Export](plaene/AP09-ergebnis-export.md) | 0,25 | KW 50 | grob geplant | A9 |
| AP10 | [Dashboard, Benachrichtigungen, Auswertungen](plaene/AP10-dashboard-auswertungen.md) | 0,5 | KW 47, KW 2 | grob geplant | – |
| AP11 | [Betrieb (unser Teil und ITM)](plaene/AP11-betrieb.md) | 0,5 (+ ITM 1–1,5) | KW 41–44, 50–51, 3 | in Arbeit | C10, B2, B5 |
| AP12 | [Pilot und Messung](plaene/AP12-pilot.md) | 1,5 | KW 1–2/2027 | grob geplant | A20, A21 |
| AP13 | [Designsystem aus Claude Design](plaene/AP13-designsystem.md) | 0,5 | KW 43 | detailliert, wartet auf Freigabe; Stand 0.3 liegt vor | – |
| | **Summe Kern** | **≈ 11,25** | | | |
| OP1 | [Option: Bewertungsmatrix](plaene/OP-optionen.md) | 0,5 | KW 51 | grob geplant | A14 |
| OP2 | [Stellungnahme BÜW](plaene/OP-optionen.md) | – | Phase 2 | vertagt (A15) | – |
Bereits erledigt (KW 40–41):
- Entwicklungsumgebung, Grundgerüst mit TallStackUI und Mehrsprachigkeit
- Server-Anforderungen, Schlüssel-Proxy mit Sperren, gitleaks-Regel
- Datenmodell Kern. Es wurde vor dem Teilplan umgesetzt; das Review steht in AP1, Schritt 1.4.
## 7. Zeitplan nach Kalenderwochen
| KW | Datum | Du + Claude Code | ITM-Entwickler | Kunde / gemeinsam |
|---|---|---|---|---|
| 40 | 28.09.–04.10. | ✓ Umgebung, Grundgerüst, TallStackUI, Mehrsprachigkeit, Doku | – | ✓ Besprechung BauIn (02.10.) |
| 41 | 05.–11.10. | ✓ Server-Anforderungen, Schlüssel-Proxy, Datenmodell; Plan und Teilpläne; Freigaben; Meilisearch-Test Teil 1; GAEB-Test; CI; Rechtekonzept | Server-Anforderungen erhalten; Schlüssel | ✓ Fragen raus, erste Antworten (05.10.); ✓ Stand 0.3 von Claude Design (05.10.) |
| 42 | 12.–18.10. | Rechte und Policies; Benutzer- und Projektverwaltung; Upload; KI-API Schnelltest und Einstellungen; Statusbericht | Staging aufbauen | Aufwand an ITM (14.10.); **Termin BauIn 16.10.** |
| 43 | 19.–25.10. | Antworten einarbeiten, Teilpläne AP4/AP6 detaillieren; GAEB-Import, LV-Ansicht; Protokoll; Designsystem | Staging: Dienste, TLS | – |
| 44 | 26.10.–01.11. | PDF-Anzeige mit Sprung; KI-Clients und Jobs; Deploy-Vorlagen; erste Bereitstellung | Staging fertig | **M1 Demo 30.10.** |
| 45 | 02.–08.11. | Indexierung, hybride LV-Suche mit Rechtefilter; Nachtrags-Import | Unterstützung | – |
| 46 | 09.–15.11. | Suchreihenfolge, LV-Suche-Oberfläche; Vorprüfung Stufe 1; Meilisearch-Test Teil 2 | – | Pilotdaten auf Staging (du) |
| 47 | 16.–22.11. | Prüfmaske v4 ohne Sprachmodell, Feedback; Dashboard (Teil 1) | – | **M2 Prüftermin 20.11.** |
| 48 | 23.–29.11. | Sprachmodell: Einschätzung, Bezugsposition, strukturierte Antwort | – | – |
| 49 | 30.11.–06.12. | Begründung mit Quellen; Referenzfälle; Regeln beginnen | – | Demo 04.12. |
| 50 | 07.–13.12. | Regeln und Freigaben; Ergebnisliste, Excel-Export | Produktionsserver | – |
| 51 | 14.–20.12. | OP1 Bewertungsmatrix (falls Vorlage und Stunden); Feinschliff | Produktion fertig | **M3 Präsentation 18.12.** |
| 52–53 | 21.12.–03.01. | Pause | Pause | – |
| 1 | 04.–10.01. | Pilot: alle Nachträge des Pilot-PFA, messen | – | Fachexperte bewertet |
| 2 | 11.–17.01. | Nachschärfen; Dashboard, Auswertungen | – | Demo 15.01. |
| 3 | 18.–24.01. | Produktion: Inbetriebnahme, Backups, Alarmierung, Restore-Test | Backups, Restore-Test | – |
| 4 | 25.–31.01. | Auswertung, Übergabe, Doku | – | **M4 Abschluss 29.01.** |
| 5–6 | 01.–14.02. | Puffer | Puffer | – |
## 8. Zulieferungen
| Bis | Von | Was | Für |
|---|---|---|---|
| ✓ 02.10. | ITM | API-Dokumentation | AP3 |
| KW 41 | ITM | API-Schlüssel (nur Produktion, C1), Antworten C4–C9 | AP3 |
| KW 41 | BauIn | Antworten auf die ★-Fragen | Teilpläne |
| ✓ KW 41 | wir → ITM | Server-Anforderungen (noch verschicken) | AP11 |
| ✓ 05.10. | Claude Design | Stand 0.3 nach unserer Rückmeldung (`claude-design/2026-10-05_Stand-0.3/` im Planungsordner) | AP13 |
| 16.10. | BauIn | Matrix-Vorlage (leer und ausgefüllt), anonymisierter Beispielablauf | AP5, AP9, OP1 |
| KW 43 | BauIn | Entscheidung Rollen, Freigabe-Variante | AP1, AP8 |
| ✓ 05.10. | BauIn | Echte Pilotdaten dürfen bei ITM liegen und über die KI-API laufen (A19); ab wann, ist offen | M2 |
| KW 44 | ITM | Staging-Server mit Zugang, Domain, Zertifikat | M2 |
| KW 46 | BauIn | Pilotdaten: alle LVs eines PFA (X86 + PDF), nur auf Staging | M2 |
| KW 47 | BauIn | 10–20 abgeschlossene Nachträge mit Ergebnis | AP2, AP12 |
| KW 51 | ITM | Produktionsserver | AP11 |
| vor Livegang | BauIn, ITM | Zugriff, Adresse, Backups, Datenschutz (B2, B3, B5, B6) | AP11 |
## 9. Risiken
| Risiko | Auswirkung | Gegenmaßnahme |
|---|---|---|
| Zulieferungen kommen spät | Pakete verschieben sich | Termine am 16.10. vereinbaren; Unabhängiges vorziehen |
| Vorgaben zu Datenschutz und Informationssicherheit (B6) kommen erst vor dem Livegang | Nacharbeit kurz vor dem Livegang | Grundsätze schon jetzt einhalten (Rechte, Protokoll, alles lokal außer KI-API); Liste in AP11 |
| Nur Produktionsschlüssel (C1) | Entwicklung und Tests verbrauchen Limit und Kosten der Produktion | Tests mit `Http::fake`, Live-Aufrufe sparsam, Ratenbegrenzer (AP3) |
| Vibe Coding: Fehler in Rechten oder Suchfiltern fallen nicht auf | Unbefugte sehen vertrauliche Inhalte | Tests ohne Berechtigung sind Pflicht; CI; diese Stellen gezielt im Browser prüfen |
| Claude Code sieht keine echten Daten | Nachschärfen dauert länger | Messskripte mit Kennzahlen; anonymisierte Problemfälle |
| Nur eine Person kennt die Anwendung | Ausfall stoppt das Projekt | Doku, Teilpläne, Tests aktuell halten |
| X86 weicht vom Standard ab | GAEB-Import aufwendiger | Beispieldatei früh (A18); D86 als Rückfall |
| Sprachmodell antwortet unzuverlässig strukturiert | Stufe 2 schwächer | Prüfung im Code; Stufe 1 allein schon nützlich |
| Meilisearch mit 4.096 Dimensionen zu langsam | Suche muss umgebaut werden | Test KW 41; Plan B Qdrant |
| KI-API ausgelastet (120/min gemeinsam) | Wartezeiten | Asynchron, Stapel, Vorrang für Suchanfragen (C8) |
| Excel-Vorlage der DB mit Makros | OP1 schwieriger | Vorlage früh testen |
| Fachexperte wenig verfügbar | Keine Messung | Feste Freitagstermine, Referenzfälle früh |
## 10. Entscheidungen
| Datum | Entscheidung | Wer |
|---|---|---|
| 01.10. | Laravel 13, Livewire 4, TallStackUI 4, PHP 8.5, MySQL + Meilisearch, kein Redis | du |
| 02.10. | Phase 1 nur „dem Grunde nach“, Nachträge statt MKA, Höhe Phase 2, Regelwerk Phase 3 | BauIn, du |
| 02.10. | Team: du + Claude Code (Vibe Coding), ITM-Entwickler für KI-API und Server | du |
| 05.10. | API-Schlüssel im Windows-Tresor mit Schlüssel-Proxy; in der App verschlüsselt in der DB, nie in `.env` | du, ITM |
| 05.10. | Projektrollen in eigener Tabelle, globale Rollen über Spatie (Review in AP1, Schritt 1.4) | Claude Code, Freigabe offen |
| 05.10. | Strukturiertes Vorgehen: nur Umsetzung nach freigegebenem Teilplan | du |
| 05.10. | Anmeldung mit eigenem Passwort, 2FA später Pflicht (B1); keine E-Mails (B3); Mehrfach-Upload ohne ZIP (B4) | BauIn, du |
| 05.10. | Anordnungen außen vor (A13), DOXIS von Hand (A16), Stellungnahme BÜW und Österreich in Phase 2 (A15, A26) | BauIn, du |
| 05.10. | Pilotdaten dürfen bei ITM liegen und über die KI-API laufen (A19); Preise, Datenverarbeitung und Server sind Sache von ITM (C2, C3, C10) | du, ITM |
| 05.10. | Abgrenzung mit dem ITM-Entwickler wie vorgeschlagen (C12); Gitea von ITM: `https://gitea.itm-technologies.de/ChristophGraf/BauIN` (C11) | ITM |
| 05.10. | Claude Design Stand 0.3 ist verbindlich für die Oberfläche; Abweichungen stehen in AP13 | du |
Offene Entscheidungen stehen in den Teilplänen unter „Entscheidungen“.
## 11. Nach Phase 1
- **Phase 2 – Prüfung der Höhe nach:**
- Preis gegen Bezugsposition und gleiche Leistung in anderen PFAs vergleichen.
- Auszüge der Urkalkulation und Nachtragskalkulation einbeziehen.
- Ebenfalls Phase 2: Stellungnahme BÜW als Word-Datei (A15), Österreich (A26).
- **Phase 3 – Regelwerk:**
- Richtlinien der DB (263 PDFs, 743 MB) in die Suche aufnehmen.
- Interner Chat, der mit PDF, Seite und Stelle zitiert.
- **Weitere Ideen:**
- Dokumentenmanagement, E-Mail- und DOXIS-Anbindung
- Österreich im Detail: ÖNORM B 2110/B 2118, A 2063
- Bauzeitenplan-Analyse, weitere Sprachen
- **Nach dem Livegang von Phase 1 klären:** Aufbewahrung nach Projektende (A27), Nutzung der
DB-Richtlinien in einem KI-System (A28).
- **Nebenprojekt datenschleuse:** Phase 1–2 muss fertig sein, bevor echte Kundendateien mit Claude
bearbeitet werden. Sie hilft beim anonymisierten Beispielablauf.
## 12. Änderungen an diesem Plan
| Datum | Änderung |
|---|---|
| 01.10. | Erster Plan (MVP, 2 Entwickler) |
| 02.10. | Nach Besprechung mit BauIn: Umfang, Gliederung, Optionen, Ausbaustufen; Team du + Claude Code; 30 h/Woche |
| 05.10. | Neu gegliedert: Arbeitsweise mit Freigaben, Meilensteine mit Abnahmekriterien, Teilpläne je Arbeitspaket, Fragenliste mit Status in `docs/fragen.md` |
| 05.10. | Erste Antworten eingearbeitet (B1, B3, B4, C1, A15, A19 u. a.); OP2 in Phase 2; Claude Design Stand 0.3 erhalten, Abgleich in AP13 |
+132
View File
@@ -0,0 +1,132 @@
# KI-BauIN – Server-Anforderungen (Staging und Produktion)
Stand: 5. Oktober 2026. Für den ITM-Entwickler, der die Server aufbaut. Hintergründe stehen in
`docs/tech-stack.md`, Termine in `docs/projektplan.md`.
## Überblick
- **Zwei Umgebungen, gleich aufgebaut:** Staging (Pilot mit BauIn, ab KW 44) und Produktion
(ab KW 51). Getrennte Server, getrennte Datenbanken, getrennte API-Schlüssel.
- **Ein Server je Umgebung reicht:** Anwendung, MySQL, Meilisearch und Worker laufen auf derselben VM.
- **Betrieb nativ**, ohne Docker: Nginx, PHP-FPM, Worker unter systemd, Cron für den Scheduler.
- **Die Versionen entsprechen der Entwicklung.** Abweichungen bitte vorher abstimmen.
## Ausstattung je Server (Vorschlag)
| | Vorschlag | Anmerkung |
|---|---|---|
| Betriebssystem | Ubuntu Server 24.04 LTS | Debian 12/13 geht auch |
| CPU | 4 vCPU | |
| RAM | 16 GB | Meilisearch hält Vektoren mit 4.096 Dimensionen; endgültig nach dem Meilisearch-Test (KW 41) |
| Platte | 150 GB SSD, erweiterbar | Daten (MySQL, Uploads, Meilisearch) am besten auf eigenem Volume |
| Verschlüsselung | Festplattenverschlüsselung gewünscht | Entscheidung mit BauIn offen |
| Zeitzone | Europe/Berlin, NTP aktiv | |
## Software
| Paket | Version | Hinweise |
|---|---|---|
| PHP (FPM und CLI) | 8.5 | Erweiterungen: bcmath, ctype, curl, dom, fileinfo, gd, iconv, intl, mbstring, opcache, openssl, pcntl, pdo_mysql, simplexml, sodium, tokenizer, xml, xmlreader, xmlwriter, zip, zlib |
| Composer | 2.x | |
| Node.js mit npm | 22 LTS | nur zum Bauen der Oberfläche beim Deployment |
| MySQL | 8.4 LTS | utf8mb4 / utf8mb4_unicode_ci, nur auf 127.0.0.1, `max_allowed_packet` mindestens 64M |
| Meilisearch | v1.54.2 (fest) | eigener systemd-Dienst, nur auf 127.0.0.1:7700, Master-Key gesetzt, `--no-analytics` |
| Nginx | aktuelle Version der Distribution | `client_max_body_size 200M` (ZIP mit LVs), HTTP → HTTPS |
| poppler-utils, qpdf | Distribution | Text und Seitenzahlen aus PDFs lesen, große PDFs teilen (Grenze der KI-API: 50 MB) |
| git, unzip | Distribution | Deployment aus Gitea |
## Dienste
- `php8.5-fpm`: Pool läuft unter einem eigenen Benutzer (Vorschlag `kibauin`).
- `nginx`
- `mysql`
- `meilisearch`: eigener Benutzer, Daten auf dem Datenvolume.
- **Worker:** zwei Instanzen `php artisan queue:work database --sleep=3 --tries=3 --max-time=3600`
unter systemd, Benutzer `kibauin`, mit automatischem Neustart.
- **Scheduler:** Cron-Zeile `* * * * * cd /srv/ki-bauin/current && php artisan schedule:run`.
Die Vorlagen für den Nginx-Server-Block, die systemd-Units, die Cron-Zeile, das Deploy-Skript und
das Backup-Skript liefern wir bis KW 44 im Repository (`deploy/`).
## Netzwerk
- **Eingehend:** nur 443 sowie 80 für die Weiterleitung und die Zertifikatsausstellung.
SSH nur mit Schlüssel und möglichst nur über VPN oder freigegebene IP-Adressen.
- **Zugriff für BauIn:** offen, ob aus dem Internet mit Login und 2FA oder nur über VPN
bzw. freigegebene IP-Adressen (Fragenkatalog B2).
- **Ausgehend:**
- `https://api-werk.de` (KI-API)
- der Gitea von ITM (Deployment)
- SMTP für Benachrichtigungen
- Paketquellen für Updates
Die Anwendung selbst lädt nichts von fremden Servern.
- **MySQL und Meilisearch** sind nur lokal erreichbar.
## Verzeichnisse
```
/srv/ki-bauin/
├── releases/<zeitstempel>/ je Deployment ein Verzeichnis
├── current -> releases/… aktive Version (Symlink)
└── shared/
├── .env Rechte 0600, Besitzer kibauin
└── storage/ u. a. storage/app/private = Uploads (nie über Nginx ausliefern)
```
## Konfiguration und Geheimnisse
- **In `shared/.env`** stehen `APP_KEY`, das Datenbank-Passwort, der Meilisearch-Master-Key und
die Mail-Zugangsdaten.
- **Die Schlüssel der KI-API stehen nicht in der `.env`.** Sie werden in der Anwendung unter
Verwaltung → Einstellungen eingetragen und verschlüsselt in der Datenbank gespeichert. Danach
sind sie nur noch beschreibbar, nicht mehr lesbar.
- **Der `APP_KEY` muss zusätzlich sicher außerhalb des Servers aufbewahrt werden**, z. B. im
Passwortmanager. Ohne ihn lassen sich verschlüsselte Felder aus einem Backup nicht wiederherstellen.
## Backups (Vorschlag, Entscheidung mit BauIn offen)
- **Was:** täglich `mysqldump --single-transaction`, dazu `shared/storage/app/private` und
`shared/.env`.
- **Wie:** vor dem Verlassen des Servers verschlüsseln (z. B. restic), Ziel außerhalb des Servers.
- **Aufbewahrung:** z. B. 7 tägliche, 4 wöchentliche, 6 monatliche Stände.
- **Meilisearch** wird nicht gesichert, der Index wird aus MySQL neu aufgebaut.
- **Restore-Test:** einmal vollständig vor dem Produktivstart (KW 3/2027).
## Überwachung
- **Lebenszeichen:** `https://<domain>/up` antwortet mit HTTP 200.
- **Worker und Scheduler** melden Lebenszeichen, fehlgeschlagene Jobs werden gezählt. Die
Artisan-Befehle dafür liefern wir.
- **Alarm per Mail**, wenn die Anwendung, ein Worker oder der Scheduler ausfällt, wenn die Platte
zu über 80 % voll ist oder wenn die KI-API nicht erreichbar ist.
## Deployment
Ablauf, als Skript geliefert:
1. Code aus dem Gitea holen.
2. `composer install --no-dev --optimize-autoloader`
3. `npm ci && npm run build`
4. `php artisan migrate --force`
5. Konfiguration, Routen und Views cachen.
6. `php artisan queue:restart`
7. PHP-FPM neu laden.
Wer im Betrieb deployt und Updates einspielt, ist noch offen (Fragenkatalog C12).
## Termine
| Bis | Was |
|---|---|
| KW 43 | Staging-Server mit SSH-Zugang und Grundinstallation |
| KW 44 | Staging fertig: Domain, Zertifikat, Dienste, erstes Deployment |
| KW 51 | Produktionsserver fertig |
| KW 3/2027 | Backups und Alarmierung in Produktion, Restore-Test |
## Offen
- Standort und Rechenzentrum der Server, Festplattenverschlüsselung
- Domain(s) und Zertifikat (Let's Encrypt oder eigenes)
- Ziel und Zuständigkeit der Backups
- Zugriff für BauIn: Internet oder VPN
- SMTP-Server und Absenderadresse
+40 -20
View File
@@ -1,9 +1,18 @@
# KI-BauIN – Tech-Stack
Stand: 1. Oktober 2026. Grundlage ist die allgemeine Tech-Stack-Referenz einer bestehenden
Stand: 2. Oktober 2026. Grundlage ist die allgemeine Tech-Stack-Referenz einer bestehenden
Laravel-Verwaltungsanwendung (Kollegenprojekt). Dieses Dokument übernimmt deren Grundsätze
und benennt, wo KI-BauIN bewusst abweicht und warum.
## Beteiligte
- **ITM:** Anbieter der KI-API (über API-Werk: Dokumentdienst mit OCR, Embeddings, Reranker,
Sprachmodell), Projektleitung und abrechnende Firma. Stellt auch die Server (Staging, Produktion) und den Gitea, auf den das
Repository gespiegelt wird.
- **BauIn:** Endkunde. Prüft als Bauüberwachung im Auftrag der DB Nachträge dem Grunde nach.
Liefert Fachdaten (LVs, Nachträge, Bewertungsmatrizen), stellt den Fachexperten und nutzt das System.
- „Kunde“ meint in diesem Dokument BauIn; wo ITM gemeint ist, steht ITM.
## 1. Backend und Oberfläche
| Technologie | Einsatz | Vergleich zur Referenz |
@@ -14,11 +23,14 @@ und benennt, wo KI-BauIN bewusst abweicht und warum.
| Blade | Templates, Layouts, eigene Komponenten | gleich |
| Tailwind CSS 4 | Gestaltung | gleich |
| TallStackUI 4 (Präfix `ts-`) | UI-Komponenten | Version abweichend (Referenz: 3) |
| Laravel Fortify | Login, Passwort-Reset, E-Mail-Bestätigung, 2FA, Passkeys | ergänzt |
| Laravel Fortify | Login, 2FA, Passkeys; ohne E-Mail-Funktionen (keine E-Mails laut BauIn) | ergänzt |
| Mehrsprachigkeit | Deutsch (Standard) und Englisch, Sprache je Benutzer; Übersetzungen über Laravel Lang (nur Entwicklung), eigene Texte in `lang/project/` | ergänzt |
| Vite 8, Node.js/npm | Asset-Build | gleich |
| Composer | Abhängigkeiten mit Lockfile | gleich |
| PhpSpreadsheet | Excel-Export der Bewertungsmatrix | geplant, sobald die Vorlage vorliegt |
| GAEB-Import | LVs und Nachtrags-LVs aus GAEB DA XML (X86); D86 nur als Rückfall | ergänzt |
| pdf.js (lokal ausgeliefert, kein CDN) | Anzeige der LV- und Nachtrags-PDFs mit Sprung zur Fundstelle | ergänzt |
| PhpSpreadsheet | Ergebnisliste als Excel; Option: Bewertungsmatrix in der Vorlage der DB | geplant |
| PHPWord | Stellungnahme aus Word-Vorlage (nur .docx) | Phase 2 |
| Dompdf | PDF-Erzeugung | vorerst nicht nötig |
Aufbau als modularer Laravel-Monolith: Livewire-Komponenten für die Interaktion, Services für
@@ -35,8 +47,8 @@ Länderspezifisches (DE/AT) hinter Schnittstellen im Kern, Umsetzungen in `app/L
| Datenbank-Warteschlange | Dokumentverarbeitung, Embeddings, KI-Aufrufe | gleich |
| Laravel Scheduler | Wiederanlauf hängender Aufträge, Aufräumarbeiten | gleich |
| Meilisearch | Hybride Suche (Volltext + Vektoren) mit Rechte-Filtern; Index jederzeit aus MySQL neu aufbaubar | ergänzt |
| KI-API des Kunden | OCR, Embeddings (Qwen3-Embedding-8B), Reranker | ergänzt; Anbindung folgt |
| Persistenter Ereigniseingang mit HMAC-Prüfung | Falls die KI-API Ergebnisse per Webhook meldet | geplant, abhängig von der API |
| KI-API ITM (API-Werk, `https://api-werk.de`) | Dokumentdienst (OCR-Gateway/OpenDataLoader: PDF → Markdown/JSON, asynchron mit Abfrage des Ergebnisses), Modelle `embed`, `rerank`, `chat`, `chat-noreasoning` (OpenAI-kompatibel). Drei Schlüssel, nur im Backend; 120 Anfragen/min je Schlüssel; 50 MB je Datei; keine Idempotenz | ergänzt; Anbindung folgt |
| Persistenter Ereigniseingang mit HMAC-Prüfung | – | nicht nötig: API-Werk meldet nicht per Webhook, Ergebnisse werden abgefragt |
| Nachrichten-Outbox | – | vorerst nicht nötig; Benachrichtigungen über die Warteschlange |
Redis wird nicht eingesetzt: Warteschlange, Cache und Sessions laufen über die Datenbank.
@@ -47,18 +59,21 @@ Redis wird nicht eingesetzt: Warteschlange, Cache und Sessions laufen über die
|---|---|---|
| Anmeldung | Fortify, serverseitige Sitzungen; keine Selbstregistrierung | gleich |
| Rollen und Rechte | Spatie laravel-permission | gleich |
| 2FA | TOTP über Fortify (intern Google2FA), Wiederherstellungscodes | gleich; Pflicht für alle noch offen |
| 2FA | TOTP über Fortify (intern Google2FA), Wiederherstellungscodes | gleich; zunächst freiwillig, später Pflicht per Schalter |
| Passkeys | WebAuthn über Fortify | ergänzt |
| API-Authentifizierung | – | Sanctum vorerst nicht nötig (keine eigene API) |
| Verschlüsselung sensibler Werte | Verschlüsselte Casts, u. a. für Zugangsdaten der KI-API | gleich |
| Schlüssel der KI-API | Nie in `.env`: verschlüsselt in der Datenbank, in der Oberfläche nur beschreibbar; in der Entwicklung Schlüssel-Proxy mit Windows-Anmeldeinformationsverwaltung (`tools/ki-proxy`) | ergänzt |
| Transport | HTTPS/TLS | gleich |
| Formularschutz | CSRF, serverseitige Validierung | gleich |
| Protokollierung | Verwaltungsaktionen (Upload, Rechte, Freigaben) und technische Logs | gleich |
| Geheimnisse im Code | gitleaks als Pre-Commit-Hook | ergänzt |
| Geheimnisse im Code | gitleaks als Pre-Commit-Hook, eigene Regel für API-Werk-Schlüssel (`.gitleaks.toml`) | ergänzt |
| Keine externen Ressourcen | Test prüft alle Seiten auf fremde Hosts | ergänzt (Vorgabe „alles lokal“) |
Rechte werden an den Zugriffswegen und an den auslösenden Aktionen geprüft, zusätzlich bei
Suchtreffern und bevor Inhalte an ein Sprachmodell gehen.
Suchtreffern (vor dem Reranking) und bevor Inhalte an ein Sprachmodell gehen. Dokumentinhalte
sind für das Sprachmodell Daten, keine Anweisungen. Keine Authorization-Köpfe, ganzen Dokumente
oder Prompts in technischen Logs.
## 4. Entwicklung und Qualität
@@ -68,7 +83,7 @@ Suchtreffern und bevor Inhalte an ein Sprachmodell gehen.
| Tests | Pest 4 auf PHPUnit 12; Tests im PHPUnit-Klassenstil | gleich (Runner ergänzt) |
| Code-Stil | Laravel Pint | gleich |
| Statische Analyse | PHPStan/Larastan Stufe 7 | ergänzt |
| Versionsverwaltung | Git, Gitea (lokal führend, Kunde als Push-Mirror) | gleich |
| Versionsverwaltung | Git, Gitea (lokal führend; Push-Mirror nach `https://gitea.itm-technologies.de/ChristophGraf/BauIN`) | gleich |
## 5. Betrieb (Produktion, geplant)
@@ -80,13 +95,14 @@ PHP, MySQL und Meilisearch sind in Entwicklung und Produktion gleich.
Ergänzt bzw. anders als in der Referenz:
- **Externe Alarmierung von Anfang an** (z. B. Mail), wenn Worker oder Scheduler ausfallen,
Jobs fehlschlagen oder die KI-API nicht erreichbar ist.
- **Backups verschlüsselt**, bevor sie den Server verlassen (z. B. restic). Speicherort und
Aufbewahrungsdauer legt der Kunde fest. Gesichert werden Datenbank, private Uploads und
- **Externe Alarmierung von Anfang an**, wenn Worker oder Scheduler ausfallen, Jobs fehlschlagen
oder die KI-API ITM nicht erreichbar ist. Die Anwendung verschickt keine E-Mails; der Weg wird
mit ITM abgestimmt (AP11, E11.3).
- **Backups verschlüsselt**, bevor sie den Server verlassen (z. B. restic). Speicherort,
Aufbewahrungsdauer und Zuständigkeit sind noch offen. Gesichert werden Datenbank, private Uploads und
Konfiguration; der Meilisearch-Index nicht, er wird aus MySQL neu aufgebaut.
- **Wiederanlauf auf einem Ersatzserver** einmal vollständig testen; Datenverlust und
Wiederanlaufzeit mit dem Kunden festlegen.
Wiederanlaufzeit mit BauIn und ITM festlegen.
## 6. Bewusste Abweichungen von der Referenz
@@ -95,16 +111,20 @@ Ergänzt bzw. anders als in der Referenz:
| Livewire | 3 | 4 | Neues Projekt; das Starter-Kit von Laravel 13 baut auf Livewire 4 auf, ein späterer Umstieg entfällt. |
| TallStackUI | 3 | 4 | TallStackUI 3 läuft nur mit Livewire 3; Version 4 setzt Livewire ≥ 4.3 voraus. |
| PHP | 8.4 | 8.5 | Sicherheitsupdates bis Ende 2029 statt Ende 2028. |
| Backups | unverschlüsselt, Hetzner Object Storage | verschlüsselt, Ort nach Vorgabe des Kunden | Vertrauliche Vertragsunterlagen; Vorgabe „alles lokal“. |
| Backups | unverschlüsselt, Hetzner Object Storage | verschlüsselt, Ort noch offen | Vertrauliche Vertragsunterlagen; Vorgabe „alles lokal“. |
| Suche | – | Meilisearch | Hybride Suche und Vektorsuche für die Nachtragsprüfung. |
## 7. Offene Punkte
- Ist 2FA für alle Benutzer Pflicht?
- Verlangt die IT des Kunden eine Festplattenverschlüsselung auf der VM?
- Speicherort und Aufbewahrungsdauer der Backups.
Vor dem Livegang zu klären (Liste in `docs/plaene/AP11-betrieb.md`): Zugriff, Adresse, Backups,
Datenschutz und Informationssicherheit, Einschalten der 2FA-Pflicht. Server, Verschlüsselung und
Datenverarbeitung bei API-Werk sind Sache von ITM.
- Weitere Sprachen über Deutsch und Englisch hinaus (technisch vorbereitet, fachlich nicht angefragt).
Geklärt am 05.10.: eigenes Passwort statt Microsoft-Konto, 2FA zunächst freiwillig und später
Pflicht, keine E-Mails, nur Schlüssel für die Produktion.
## Architekturübersicht
```mermaid
@@ -117,12 +137,12 @@ flowchart LR
App --> Search[(Meilisearch)]
App --> Queue[Datenbank-Warteschlange]
Queue --> Worker[Worker unter systemd]
Worker --> KI[KI-API des Kunden: OCR, Embeddings, Reranker]
Worker --> KI[KI-API ITM über API-Werk: OCR, Embeddings, Reranker, Sprachmodell]
Worker --> DB
Worker --> Search
Cron[Cron] --> Scheduler[Laravel Scheduler]
Scheduler --> Queue
Health[Statusprüfung und Alarmierung] --> Worker
Health --> Scheduler
Backup[Verschlüsselte Sicherung] --> Storage[Speicherort nach Vorgabe des Kunden]
Backup[Verschlüsselte Sicherung] --> Storage[Speicherort noch offen]
```
@@ -0,0 +1,98 @@
<?php
namespace Tests\Feature\Datenmodell;
use App\Enums\DokumentKategorie;
use App\Enums\NachtragStatus;
use App\Enums\Projektrolle;
use App\Models\Abschnitt;
use App\Models\Auftragnehmer;
use App\Models\Dokument;
use App\Models\Nachtrag;
use App\Models\Nachtragsposition;
use App\Models\Projekt;
use App\Models\User;
use App\Models\Vertrag;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class BeziehungenTest extends TestCase
{
use RefreshDatabase;
public function test_auftragnehmer_kann_in_mehreren_abschnitten_vertraege_haben(): void
{
$projekt = Projekt::factory()->create();
$auftragnehmer = Auftragnehmer::factory()->create(['mandant_id' => $projekt->mandant_id]);
[$pfa3, $pfa4] = Abschnitt::factory()->count(2)->create(['projekt_id' => $projekt->id]);
Vertrag::factory()->create(['abschnitt_id' => $pfa3->id, 'auftragnehmer_id' => $auftragnehmer->id]);
Vertrag::factory()->create(['abschnitt_id' => $pfa4->id, 'auftragnehmer_id' => $auftragnehmer->id]);
$this->assertEqualsCanonicalizing(
[$pfa3->id, $pfa4->id],
$auftragnehmer->vertraege()->pluck('abschnitt_id')->all(),
);
$this->assertSame(2, $projekt->vertraege()->count());
}
public function test_nachtrag_hat_mka_bezug_status_und_positionen_in_reihenfolge(): void
{
$nachtrag = Nachtrag::factory()->create(['mka_bezug' => ['MKA 131', 'MKA 133']]);
Nachtragsposition::factory()->create(['nachtrag_id' => $nachtrag->id, 'oz' => 'N1.0020', 'sortierung' => 2]);
Nachtragsposition::factory()->create(['nachtrag_id' => $nachtrag->id, 'oz' => 'N1.0010', 'sortierung' => 1]);
$nachtrag = $nachtrag->fresh();
$this->assertNotNull($nachtrag);
$this->assertSame(['MKA 131', 'MKA 133'], $nachtrag->mka_bezug);
$this->assertSame(NachtragStatus::Eingegangen, $nachtrag->status);
$this->assertSame(['N1.0010', 'N1.0020'], $nachtrag->positionen->pluck('oz')->all());
}
public function test_nachtragsnummer_ist_je_vertrag_eindeutig(): void
{
$vertrag = Vertrag::factory()->create();
Nachtrag::factory()->create(['vertrag_id' => $vertrag->id, 'nummer' => 'N14']);
$this->expectException(QueryException::class);
Nachtrag::factory()->create(['vertrag_id' => $vertrag->id, 'nummer' => 'N14']);
}
public function test_projektmitglieder_haben_eine_rolle(): void
{
$projekt = Projekt::factory()->create();
$leiterin = User::factory()->create();
$leser = User::factory()->create();
$aussenstehend = User::factory()->create();
$projekt->mitglieder()->attach($leiterin, ['rolle' => Projektrolle::Projektleiter->value]);
$projekt->mitglieder()->attach($leser, ['rolle' => Projektrolle::Leser->value]);
$this->assertSame(Projektrolle::Projektleiter, $projekt->rolleVon($leiterin));
$this->assertSame(Projektrolle::Leser, $projekt->rolleVon($leser));
$this->assertNull($projekt->rolleVon($aussenstehend));
$this->assertTrue($leiterin->projekte()->whereKey($projekt->id)->exists());
}
public function test_vertrauliches_dokument_hat_benannte_freigaben(): void
{
$nachtrag = Nachtrag::factory()->create();
$dokument = Dokument::factory()->zu($nachtrag, DokumentKategorie::Anlage)->vertraulich()->create();
$berechtigt = User::factory()->create();
$dokument->freigaben()->attach($berechtigt);
$this->assertTrue($dokument->vertraulich);
$this->assertSame([$berechtigt->id], $dokument->freigaben()->pluck('users.id')->all());
$this->assertSame(1, $nachtrag->dokumente()->count());
}
public function test_archiviertes_projekt_wird_erkannt(): void
{
$this->assertTrue(Projekt::factory()->archiviert()->create()->istArchiviert());
$this->assertFalse(Projekt::factory()->create()->istArchiviert());
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tests\Feature\Datenmodell;
use App\Enums\Projektrolle;
use App\Models\Nachtrag;
use App\Models\Projekt;
use App\Models\User;
use Database\Seeders\DemoSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class DemoSeederTest extends TestCase
{
use RefreshDatabase;
public function test_demodaten_entsprechen_den_beispielen_und_lassen_sich_wiederholt_einspielen(): void
{
$testbenutzer = User::factory()->create(['email' => 'test@example.com']);
$this->seed(DemoSeeder::class);
$this->seed(DemoSeeder::class);
$projekt = Projekt::query()->where('name', DemoSeeder::PROJEKT)->sole();
$this->assertSame(['PFA 1', 'PFA 2', 'PFA 3', 'PFA 4'], $projekt->abschnitte->pluck('bezeichnung')->all());
$this->assertSame(8, $projekt->vertraege()->count());
$this->assertSame(['N14', 'N16', 'N21'], $projekt->nachtraege()->orderBy('nummer')->pluck('nummer')->all());
$this->assertSame(['MKA 131', 'MKA 133'], Nachtrag::query()->where('nummer', 'N21')->sole()->mka_bezug);
$this->assertSame(Projektrolle::Projektleiter, $projekt->rolleVon($testbenutzer));
$this->assertSame($projekt->mandant_id, $testbenutzer->fresh()?->mandant_id);
}
}
@@ -0,0 +1,49 @@
<?php
namespace Tests\Feature\Datenmodell;
use App\Models\Abschnitt;
use App\Models\Nachtrag;
use App\Models\Nachtragsposition;
use App\Models\Projekt;
use App\Models\User;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class LoeschenTest extends TestCase
{
use RefreshDatabase;
public function test_projekt_mit_abschnitten_laesst_sich_nicht_loeschen(): void
{
$abschnitt = Abschnitt::factory()->create();
$this->expectException(QueryException::class);
$abschnitt->projekt->delete();
}
public function test_nachtragspositionen_werden_mit_dem_nachtrag_geloescht(): void
{
$nachtrag = Nachtrag::factory()->create();
Nachtragsposition::factory()->count(3)->create(['nachtrag_id' => $nachtrag->id]);
$nachtrag->delete();
$this->assertSame(0, Nachtragsposition::query()->count());
}
public function test_geloeschter_benutzer_verschwindet_aus_projekt_und_zuweisung(): void
{
$projekt = Projekt::factory()->create();
$benutzer = User::factory()->create();
$projekt->mitglieder()->attach($benutzer, ['rolle' => 'bearbeiter']);
$nachtrag = Nachtrag::factory()->create(['zugewiesen_an' => $benutzer->id]);
$benutzer->delete();
$this->assertSame(0, $projekt->mitglieder()->count());
$this->assertNull($nachtrag->fresh()?->zugewiesen_an);
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
namespace Tests\Feature\Datenmodell;
use App\Enums\DokumentKategorie;
use App\Enums\Land;
use App\Models\Abschnitt;
use App\Models\Auftragnehmer;
use App\Models\Dokument;
use App\Models\Leistungsverzeichnis;
use App\Models\Mandant;
use App\Models\Nachtrag;
use App\Models\Nachtragsposition;
use App\Models\Projekt;
use App\Models\Vertrag;
use Illuminate\Foundation\Testing\RefreshDatabase;
use LogicException;
use Tests\TestCase;
class ZuordnungTest extends TestCase
{
use RefreshDatabase;
public function test_mandant_land_und_projekt_werden_die_kette_hinunter_uebernommen(): void
{
$projekt = Projekt::factory()->create(['land' => Land::AT]);
$abschnitt = Abschnitt::factory()->create(['projekt_id' => $projekt->id]);
$vertrag = Vertrag::factory()->create(['abschnitt_id' => $abschnitt->id]);
$lv = Leistungsverzeichnis::factory()->create(['vertrag_id' => $vertrag->id]);
$nachtrag = Nachtrag::factory()->create(['vertrag_id' => $vertrag->id]);
$position = Nachtragsposition::factory()->create(['nachtrag_id' => $nachtrag->id]);
foreach ([$abschnitt, $vertrag, $lv, $nachtrag, $position] as $datensatz) {
$this->assertSame($projekt->mandant_id, $datensatz->mandant_id, $datensatz::class);
$this->assertSame(Land::AT, $datensatz->land, $datensatz::class);
$this->assertSame($projekt->id, $datensatz->projekt_id, $datensatz::class);
}
}
public function test_dokument_uebernimmt_die_zuordnung_vom_lv(): void
{
$lv = Leistungsverzeichnis::factory()->create();
$dokument = Dokument::factory()->zu($lv, DokumentKategorie::LvX86)->create();
$this->assertSame($lv->projekt_id, $dokument->projekt_id);
$this->assertSame($lv->mandant_id, $dokument->mandant_id);
$this->assertTrue($dokument->dokumentable?->is($lv));
$this->assertSame('leistungsverzeichnis', $dokument->dokumentable_type);
}
public function test_allgemeines_dokument_ohne_projekt_braucht_mandant_und_land(): void
{
$mandant = Mandant::factory()->create();
$dokument = Dokument::factory()->create([
'projekt_id' => null,
'mandant_id' => $mandant->id,
'land' => Land::DE,
'kategorie' => DokumentKategorie::Vorlage,
]);
$this->assertNull($dokument->projekt_id);
$this->assertSame($mandant->id, $dokument->mandant_id);
}
public function test_vertrag_mit_auftragnehmer_eines_anderen_mandanten_wird_abgewiesen(): void
{
$abschnitt = Abschnitt::factory()->create();
$fremderAuftragnehmer = Auftragnehmer::factory()->create();
$this->expectException(LogicException::class);
Vertrag::factory()->create([
'abschnitt_id' => $abschnitt->id,
'auftragnehmer_id' => $fremderAuftragnehmer->id,
]);
}
public function test_abweichender_mandant_beim_anlegen_wird_abgewiesen(): void
{
$projekt = Projekt::factory()->create();
$andererMandant = Mandant::factory()->create();
$this->expectException(LogicException::class);
Abschnitt::factory()->create([
'projekt_id' => $projekt->id,
'mandant_id' => $andererMandant->id,
]);
}
public function test_nachtrag_laesst_sich_nicht_in_ein_anderes_projekt_verschieben(): void
{
$nachtrag = Nachtrag::factory()->create();
$vertragInAnderemProjekt = Vertrag::factory()->create();
$this->expectException(LogicException::class);
$nachtrag->update(['vertrag_id' => $vertragInAnderemProjekt->id]);
}
public function test_nachtrag_darf_innerhalb_des_projekts_den_vertrag_wechseln(): void
{
$nachtrag = Nachtrag::factory()->create();
$abschnitt = $nachtrag->vertrag->abschnitt;
$andererVertrag = Vertrag::factory()->create(['abschnitt_id' => $abschnitt->id]);
$nachtrag->update(['vertrag_id' => $andererVertrag->id]);
$this->assertSame($andererVertrag->id, $nachtrag->fresh()?->vertrag_id);
}
}
+65
View File
@@ -0,0 +1,65 @@
# Schlüssel-Proxy für die KI-API (nur Entwicklung)
Der Proxy setzt die API-Schlüssel von ITM in Anfragen an `https://api-werk.de` ein. Claude Code,
Tests und die App in Sail rufen den Proxy ohne Schlüssel auf und sehen ihn nie. Die Schlüssel
liegen in der Windows-Anmeldeinformationsverwaltung. Der Proxy liest sie beim Start und hält sie
nur im Arbeitsspeicher; er protokolliert Methode, Pfad, Status und Dauer, aber keine Header.
In Staging und Produktion gibt es keinen Proxy: Dort stehen die Schlüssel verschlüsselt in der
Datenbank (Verwaltung → Einstellungen).
## Einmalig: Schlüssel in den Windows-Tresor eintragen
Systemsteuerung → Anmeldeinformationsverwaltung → Windows-Anmeldeinformationen →
„Generische Anmeldeinformationen hinzufügen“:
| Internet- oder Netzwerkadresse | Benutzername | Kennwort |
|---|---|---|
| `ki-bauin/itm-ki` | `api` | KI-Schlüssel (`zki_…`) |
| `ki-bauin/dataloader` | `api` | OpenDataLoader-Schlüssel (`zodl_…`) |
| `ki-bauin/ocr` | `api` | OCR-Schlüssel (`zocr_…`) |
Schlüssel nie in ein Terminal, eine Datei oder den Chat mit Claude kopieren.
## Starten (nur du, in einem eigenen Terminal)
```bash
cd ~/code/ki-bauin
python3 tools/ki-proxy/ki_proxy.py --check # zeigt nur „gefunden“ / „FEHLT“, keine Werte
python3 tools/ki-proxy/ki_proxy.py # läuft, bis Strg+C
```
Nach dem Ändern eines Schlüssels den Proxy neu starten. Claude Code startet den Proxy nie selbst;
ein Hook in `~/.claude/settings.json` sperrt das zusätzlich.
## Adressen
| Von | Adresse |
|---|---|
| WSL (Claude Code, Skripte) | `http://127.0.0.1:8787` |
| App in Sail (Container) | `http://host.docker.internal:8787` |
Pfade wie bei api-werk.de: `/v1/ki/…`, `/v1/dataloader/…`, `/v1/ocr/…`. Andere Pfade lehnt der
Proxy ab. Über die Netzwerkkarte der WSL und aus dem LAN ist er nicht erreichbar.
```bash
curl -s http://127.0.0.1:8787/v1/ki/models
```
## Was der Proxy schützt und was nicht
- **Geschützt:** Der Schlüssel steht in keiner Datei, keiner Umgebungsvariablen und keinem
Startbefehl. Den Speicher des Proxys können andere Prozesse nicht lesen (`ptrace_scope=1`).
Ein Hook sperrt für Claude Code Windows-Programme (`powershell.exe`, `cmdkey.exe` usw.), das
Auslesen fremder Prozesse und das Starten des Proxys.
- **Nicht geschützt:** Das ist ein Schutz vor Versehen, kein Schutz vor Absicht. Jeder Prozess
auf deinem Rechner kann den Proxy aufrufen, solange er läuft. Und der Proxy schützt die
Schlüssel, nicht die Daten: Echte Kundendaten gehen trotzdem nicht an Claude.
## Tests
```bash
python3 -m unittest discover -s tools/ki-proxy
```
Die Tests laufen gegen einen Platzhalter-Server, ohne Tresor und ohne Netz.
+300
View File
@@ -0,0 +1,300 @@
#!/usr/bin/env python3
"""Schlüssel-Proxy für die KI-API von ITM (API-Werk) – nur für die Entwicklung.
Liest beim Start die API-Schlüssel aus der Windows-Anmeldeinformationsverwaltung,
hält sie nur im Arbeitsspeicher und setzt sie in weitergeleitete Anfragen ein.
Aufrufer (Claude Code, Tests, die App in Sail) schicken Anfragen ohne Schlüssel.
Der Proxy wird ausschließlich vom Entwickler in einem eigenen Terminal gestartet,
nie von Claude Code. Er gibt Schlüssel nie aus und protokolliert keine Header.
"""
from __future__ import annotations
import argparse
import base64
import http.client
import json
import logging
import shutil
import ssl
import subprocess
import sys
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Callable, Optional
from urllib.parse import urlsplit
# Pfadpräfix → Eintrag in der Windows-Anmeldeinformationsverwaltung
ROUTES: dict[str, str] = {
"/v1/ki/": "ki-bauin/itm-ki",
"/v1/dataloader/": "ki-bauin/dataloader",
"/v1/ocr/": "ki-bauin/ocr",
}
DEFAULT_UPSTREAM = "https://api-werk.de"
DEFAULT_PORT = 8787
DEFAULT_LISTEN = ("127.0.0.1", "172.17.0.1") # WSL selbst und die Docker-Brücke (Sail)
MAX_BODY_BYTES = 60 * 1024 * 1024 # Dateigrenze der API ist 50 MB
UPSTREAM_TIMEOUT = 300
HOP_BY_HOP = {
"connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
"te", "trailer", "trailers", "transfer-encoding", "upgrade",
}
log = logging.getLogger("ki-proxy")
# Liest generische Einträge über die Windows-API CredRead. Ausgabe je Eintrag:
# "<eintrag>\t<base64 des Werts in UTF-8>" oder "<eintrag>\t-", wenn er fehlt.
_POWERSHELL_TEMPLATE = r"""
$ErrorActionPreference = 'Stop'
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public static class KiBauinTresor {
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct CREDENTIAL {
public int Flags; public int Type; public string TargetName; public string Comment;
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
public int CredentialBlobSize; public IntPtr CredentialBlob; public int Persist;
public int AttributeCount; public IntPtr Attributes; public string TargetAlias; public string UserName;
}
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool CredRead(string target, int type, int flags, out IntPtr credential);
[DllImport("advapi32.dll")]
private static extern void CredFree(IntPtr credential);
public static string Read(string target) {
IntPtr p;
if (!CredRead(target, 1, 0, out p)) { return null; }
try {
CREDENTIAL c = (CREDENTIAL)Marshal.PtrToStructure(p, typeof(CREDENTIAL));
if (c.CredentialBlobSize == 0) { return ""; }
return Marshal.PtrToStringUni(c.CredentialBlob, c.CredentialBlobSize / 2);
} finally { CredFree(p); }
}
}
'@
foreach ($t in @(__TARGETS__)) {
$v = [KiBauinTresor]::Read($t)
if ($v -eq $null) { [Console]::Out.WriteLine($t + "`t-") }
else { [Console]::Out.WriteLine($t + "`t" + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($v.Trim()))) }
}
"""
def parse_tresor_output(output: str) -> dict[str, Optional[str]]:
"""Wertet die Ausgabe des PowerShell-Skripts aus."""
result: dict[str, Optional[str]] = {}
for line in output.splitlines():
line = line.strip()
if not line or "\t" not in line:
continue
target, value = line.split("\t", 1)
result[target] = None if value == "-" else base64.b64decode(value).decode("utf-8")
return result
def read_windows_credentials(targets: list[str]) -> dict[str, Optional[str]]:
"""Liest die angegebenen Einträge aus der Windows-Anmeldeinformationsverwaltung."""
powershell = shutil.which("powershell.exe")
if powershell is None:
raise RuntimeError("powershell.exe nicht gefunden – läuft das unter WSL mit Windows-Interop?")
quoted = ", ".join("'" + t.replace("'", "''") + "'" for t in targets)
script = _POWERSHELL_TEMPLATE.replace("__TARGETS__", quoted)
encoded = base64.b64encode(script.encode("utf-16-le")).decode("ascii")
completed = subprocess.run(
[powershell, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded],
capture_output=True,
timeout=60,
)
if completed.returncode != 0:
# Die Meldung von PowerShell enthält keine Schlüssel; sie kommt in der Windows-Codepage.
detail = completed.stderr.decode("cp850", errors="replace").strip().splitlines()[:3]
raise RuntimeError("Lesen aus dem Windows-Tresor fehlgeschlagen: " + " | ".join(detail))
found = parse_tresor_output(completed.stdout.decode("ascii", errors="replace"))
return {t: found.get(t) for t in targets}
class _LimitedReader:
"""Liest genau `remaining` Bytes aus dem Eingangsstrom, damit der Request-Body gestreamt wird."""
def __init__(self, stream, length: int):
self._stream = stream
self._remaining = length
def read(self, size: int = -1) -> bytes:
if self._remaining <= 0:
return b""
if size < 0 or size > self._remaining:
size = self._remaining
chunk = self._stream.read(size)
self._remaining -= len(chunk)
return chunk
class ProxyHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.0" # Antworten enden mit dem Schließen der Verbindung; einfach für Streaming
server_version = "ki-proxy"
server: "KiProxyServer"
def do_GET(self) -> None:
self._forward()
def do_POST(self) -> None:
self._forward()
def do_PUT(self) -> None:
self._forward()
def do_PATCH(self) -> None:
self._forward()
def do_DELETE(self) -> None:
self._forward()
def log_message(self, format: str, *args) -> None: # noqa: A002 – Standardausgabe abschalten
return
def _send_json(self, status: int, payload: dict) -> None:
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _forward(self) -> None:
started = time.monotonic()
path_only = self.path.split("?", 1)[0]
target = next((t for prefix, t in ROUTES.items() if path_only.startswith(prefix)), None)
status = 0
sent = 0
try:
if target is None:
status = 404
self._send_json(404, {"error": "Unbekannter Pfad. Erlaubt: " + ", ".join(ROUTES)})
return
key = self.server.keys.get(target)
if not key:
status = 503
self._send_json(503, {"error": f"Schlüssel '{target}' fehlt im Windows-Tresor. Proxy nach dem Eintragen neu starten."})
return
if self.headers.get("Transfer-Encoding", "").lower() == "chunked":
status = 411
self._send_json(411, {"error": "Request-Body mit Content-Length senden, nicht chunked."})
return
length = int(self.headers.get("Content-Length") or 0)
if length > MAX_BODY_BYTES:
status = 413
self._send_json(413, {"error": "Request zu groß für den Proxy."})
return
headers = {
name: value
for name, value in self.headers.items()
if name.lower() not in HOP_BY_HOP | {"authorization", "host", "content-length"}
}
headers["Authorization"] = f"Bearer {key}"
if length or self.command in ("POST", "PUT", "PATCH"):
headers["Content-Length"] = str(length)
body = _LimitedReader(self.rfile, length) if length else None
conn = self.server.connect_upstream()
try:
conn.request(self.command, self.server.upstream_base_path + self.path, body=body, headers=headers)
response = conn.getresponse()
status = response.status
self.send_response(response.status, response.reason)
for name, value in response.getheaders():
if name.lower() not in HOP_BY_HOP | {"server", "date"}:
self.send_header(name, value)
self.end_headers()
while True:
chunk = response.read1(65536)
if not chunk:
break
self.wfile.write(chunk)
self.wfile.flush()
sent += len(chunk)
finally:
conn.close()
except (OSError, http.client.HTTPException) as exc:
if status == 0:
status = 502
try:
self._send_json(502, {"error": f"KI-API nicht erreichbar ({type(exc).__name__})."})
except OSError:
pass
finally:
log.info("%s %s → %s, %d Bytes, %.0f ms", self.command, path_only, status or "-", sent, (time.monotonic() - started) * 1000)
class KiProxyServer(ThreadingHTTPServer):
daemon_threads = True
def __init__(self, address: tuple[str, int], keys: dict[str, Optional[str]], upstream: str):
super().__init__(address, ProxyHandler)
self.keys = keys
parts = urlsplit(upstream)
self.upstream_scheme = parts.scheme
self.upstream_host = parts.hostname or ""
self.upstream_port = parts.port or (443 if parts.scheme == "https" else 80)
self.upstream_base_path = parts.path.rstrip("/")
self._ssl_context = ssl.create_default_context() if parts.scheme == "https" else None
def connect_upstream(self) -> http.client.HTTPConnection:
if self.upstream_scheme == "https":
return http.client.HTTPSConnection(self.upstream_host, self.upstream_port, timeout=UPSTREAM_TIMEOUT, context=self._ssl_context)
return http.client.HTTPConnection(self.upstream_host, self.upstream_port, timeout=UPSTREAM_TIMEOUT)
def start_servers(hosts: list[str], port: int, keys: dict[str, Optional[str]], upstream: str) -> list[KiProxyServer]:
servers = []
for host in hosts:
try:
server = KiProxyServer((host, port), keys, upstream)
except OSError as exc:
log.warning("Kann nicht auf %s:%d lauschen (%s) – übersprungen.", host, port, exc.strerror)
continue
threading.Thread(target=server.serve_forever, daemon=True).start()
servers.append(server)
return servers
def main(argv: Optional[list[str]] = None, reader: Callable[[list[str]], dict[str, Optional[str]]] = read_windows_credentials) -> int:
parser = argparse.ArgumentParser(description="Schlüssel-Proxy für die KI-API von ITM (nur Entwicklung).")
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
parser.add_argument("--listen", default=",".join(DEFAULT_LISTEN), help="Adressen, durch Komma getrennt")
parser.add_argument("--upstream", default=DEFAULT_UPSTREAM)
parser.add_argument("--check", action="store_true", help="Nur prüfen, welche Schlüssel im Tresor stehen, dann beenden")
args = parser.parse_args(argv)
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s", datefmt="%H:%M:%S")
keys = reader(list(ROUTES.values()))
for target in ROUTES.values():
log.info("%-22s %s", target, "gefunden" if keys.get(target) else "FEHLT")
if args.check:
return 0 if all(keys.values()) else 1
hosts = [h.strip() for h in args.listen.split(",") if h.strip()]
servers = start_servers(hosts, args.port, keys, args.upstream)
if not servers:
log.error("Keine Adresse verfügbar, Proxy beendet.")
return 1
log.info("Proxy läuft auf %s → %s (Strg+C beendet)", ", ".join(f"{s.server_address[0]}:{s.server_address[1]}" for s in servers), args.upstream)
try:
while True:
time.sleep(3600)
except KeyboardInterrupt:
log.info("Proxy beendet.")
finally:
for server in servers:
server.shutdown()
return 0
if __name__ == "__main__":
sys.exit(main())
+189
View File
@@ -0,0 +1,189 @@
"""Tests für den Schlüssel-Proxy. Ausführen: python3 -m unittest discover -s tools/ki-proxy"""
from __future__ import annotations
import base64
import http.client
import json
import logging
import os
import sys
import threading
import unittest
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
sys.path.insert(0, os.path.dirname(__file__))
import ki_proxy # noqa: E402
KEYS = {"ki-bauin/itm-ki": "test-ki-schluessel", "ki-bauin/dataloader": "test-dl-schluessel", "ki-bauin/ocr": None}
class FakeUpstream(BaseHTTPRequestHandler):
"""Platzhalter für api-werk.de: merkt sich die letzte Anfrage."""
protocol_version = "HTTP/1.1"
last: dict = {}
release_stream = threading.Event()
def log_message(self, format, *args): # noqa: A002
return
def _record(self) -> bytes:
length = int(self.headers.get("Content-Length") or 0)
body = self.rfile.read(length) if length else b""
FakeUpstream.last = {"method": self.command, "path": self.path, "headers": dict(self.headers.items()), "body": body}
return body
def do_GET(self):
self._record()
if self.path.startswith("/v1/ki/stream"):
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
self._chunk(b"data: erster\n\n")
FakeUpstream.release_stream.wait(5)
self._chunk(b"data: [DONE]\n\n")
self.wfile.write(b"0\r\n\r\n")
return
status = 409 if self.path.startswith("/v1/dataloader/jobs/") else 200
payload = json.dumps({"data": [{"id": "chat"}, {"id": "embed"}]}).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def do_POST(self):
body = self._record()
payload = json.dumps({"job_id": "abc", "bytes": len(body)}).encode()
self.send_response(202)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def _chunk(self, data: bytes) -> None:
self.wfile.write(f"{len(data):x}\r\n".encode() + data + b"\r\n")
self.wfile.flush()
class ProxyTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.upstream = ThreadingHTTPServer(("127.0.0.1", 0), FakeUpstream)
threading.Thread(target=cls.upstream.serve_forever, daemon=True).start()
upstream_url = f"http://127.0.0.1:{cls.upstream.server_address[1]}"
cls.proxy = ki_proxy.start_servers(["127.0.0.1"], 0, dict(KEYS), upstream_url)[0]
cls.port = cls.proxy.server_address[1]
@classmethod
def tearDownClass(cls):
cls.proxy.shutdown()
cls.upstream.shutdown()
def setUp(self):
FakeUpstream.last = {}
FakeUpstream.release_stream.clear()
self.logs = []
handler = logging.Handler()
handler.emit = lambda record: self.logs.append(record.getMessage())
ki_proxy.log.addHandler(handler)
ki_proxy.log.setLevel(logging.INFO)
self.addCleanup(ki_proxy.log.removeHandler, handler)
def request(self, method, path, body=None, headers=None):
conn = http.client.HTTPConnection("127.0.0.1", self.port, timeout=10)
conn.request(method, path, body=body, headers=headers or {})
return conn, conn.getresponse()
def test_setzt_den_schluessel_der_route_ein(self):
conn, response = self.request("GET", "/v1/ki/models")
self.assertEqual(200, response.status)
self.assertEqual(["chat", "embed"], [m["id"] for m in json.loads(response.read())["data"]])
self.assertEqual("Bearer test-ki-schluessel", FakeUpstream.last["headers"]["Authorization"])
conn.close()
def test_ueberschreibt_mitgeschickte_authorization(self):
conn, response = self.request("GET", "/v1/ki/models", headers={"Authorization": "Bearer fremd"})
response.read()
self.assertEqual("Bearer test-ki-schluessel", FakeUpstream.last["headers"]["Authorization"])
conn.close()
def test_upload_kommt_unveraendert_an_mit_eigenem_schluessel(self):
body = os.urandom(300_000)
headers = {"Content-Type": "multipart/form-data; boundary=x"}
conn, response = self.request("POST", "/v1/dataloader/jobs?x=1", body=body, headers=headers)
self.assertEqual(202, response.status)
self.assertEqual(len(body), json.loads(response.read())["bytes"])
self.assertEqual(body, FakeUpstream.last["body"])
self.assertEqual("/v1/dataloader/jobs?x=1", FakeUpstream.last["path"])
self.assertEqual("Bearer test-dl-schluessel", FakeUpstream.last["headers"]["Authorization"])
conn.close()
def test_reicht_fehlerstatus_durch(self):
conn, response = self.request("GET", "/v1/dataloader/jobs/abc/result?format=markdown")
self.assertEqual(409, response.status)
response.read()
conn.close()
def test_fehlender_schluessel_ergibt_503_ohne_weiterleitung(self):
conn, response = self.request("GET", "/v1/ocr/jobs/abc")
self.assertEqual(503, response.status)
self.assertIn("ki-bauin/ocr", json.loads(response.read())["error"])
self.assertEqual({}, FakeUpstream.last)
conn.close()
def test_unbekannter_pfad_ergibt_404(self):
conn, response = self.request("GET", "/admin")
self.assertEqual(404, response.status)
response.read()
self.assertEqual({}, FakeUpstream.last)
conn.close()
def test_streaming_kommt_sofort_an(self):
conn, response = self.request("GET", "/v1/ki/stream")
self.assertEqual(200, response.status)
first = response.read1(1024)
self.assertIn(b"erster", first) # kommt an, bevor der Platzhalter den Rest freigibt
self.assertNotIn(b"[DONE]", first)
FakeUpstream.release_stream.set()
rest = response.read()
self.assertIn(b"[DONE]", rest)
conn.close()
def test_protokoll_enthaelt_keine_schluessel(self):
for path in ("/v1/ki/models", "/v1/ocr/x", "/unbekannt"):
conn, response = self.request("GET", path, headers={"Authorization": "Bearer fremd"})
response.read()
conn.close()
self.assertTrue(self.logs)
for line in self.logs:
for secret in ("test-ki-schluessel", "test-dl-schluessel", "fremd", "Bearer"):
self.assertNotIn(secret, line)
class TresorAusgabeTest(unittest.TestCase):
def test_wertet_gefundene_und_fehlende_eintraege_aus(self):
wert = base64.b64encode("zki_Äbc123".encode()).decode()
output = f"ki-bauin/itm-ki\t{wert}\r\nki-bauin/ocr\t-\r\n"
self.assertEqual({"ki-bauin/itm-ki": "zki_Äbc123", "ki-bauin/ocr": None}, ki_proxy.parse_tresor_output(output))
def test_check_meldet_fehlende_schluessel_ohne_werte_auszugeben(self):
logs = []
handler = logging.Handler()
handler.emit = lambda record: logs.append(record.getMessage())
ki_proxy.log.addHandler(handler)
try:
code = ki_proxy.main(["--check"], reader=lambda targets: dict(KEYS))
finally:
ki_proxy.log.removeHandler(handler)
self.assertEqual(1, code)
joined = "\n".join(logs)
self.assertIn("FEHLT", joined)
self.assertNotIn("test-ki-schluessel", joined)
if __name__ == "__main__":
unittest.main()