Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f62541ff13 | ||
|
|
2143f4e955 | ||
|
|
6aac8b0a16 | ||
|
|
ca09d23a78 | ||
|
|
6d6f8f8f27 | ||
|
|
d9cda4a7d3 | ||
|
|
56eb2ea455 | ||
|
|
558e698e3b | ||
|
|
30d80174ac | ||
|
|
68033d7cec |
@@ -36,3 +36,7 @@ yarn-error.log
|
||||
/boost.json
|
||||
/opencode.json
|
||||
/opencode.jsonc
|
||||
|
||||
# Python (tools/)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
@@ -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_"]
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
/** Land mit eigenen Regeln (Normen, Begriffe, Anspruchsgrundlagen). */
|
||||
enum Land: string
|
||||
{
|
||||
case DE = 'DE';
|
||||
case AT = 'AT';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
/** Vertragsgrundlage eines Projekts. */
|
||||
enum Vertragsgrundlage: string
|
||||
{
|
||||
case VobB = 'vob_b';
|
||||
case Bgb = 'bgb';
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -21,5 +21,9 @@ class DatabaseSeeder extends Seeder
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
|
||||
if (app()->isLocal()) {
|
||||
$this->call(DemoSeeder::class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
- …
|
||||
@@ -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 |
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user