This commit is contained in:
Team3
2026-07-22 16:12:23 +02:00
commit f07ef9653d
851 changed files with 501480 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
<script setup>
import EinheitDetail from './EinheitDetail.vue'
defineProps({ projekt: { type: Object, required: true } })
</script>
<template>
<section v-for="k in projekt.architektur" :key="k.name" class="komponente">
<h3>{{ k.name }}</h3>
<p class="besch">{{ k.beschreibung }}</p>
<p v-if="k.beziehungen.length" class="beziehungen">
<template v-for="(b, i) in k.beziehungen" :key="i">
<span v-if="i"> · </span>{{ b.typ }} {{ b.ziel }}
</template>
</p>
<details>
<summary>{{ k.einheiten.length }} Einheiten</summary>
<EinheitDetail v-for="eid in k.einheiten" :key="eid"
:projekt-name="projekt.name" :id="eid" />
</details>
</section>
<p v-if="!projekt.architektur.length" class="leer">Keine Architektur-Sicht vorhanden.</p>
</template>
<style scoped>
.komponente {
background: var(--panel); border: 1px solid var(--rand); border-radius: 10px;
padding: 0.9rem 1.2rem; margin-bottom: 1rem;
}
.komponente h3 { margin: 0; font-size: 1.02rem; }
.besch { color: var(--dezent); font-size: 0.9rem; margin: 0.2rem 0; }
.beziehungen { color: var(--akzent); font-size: 0.85rem; margin: 0.2rem 0 0.5rem; }
summary { font-size: 0.9rem; color: var(--dezent); padding: 0.3rem 0; }
.leer { color: var(--dezent); }
</style>

View File

@@ -0,0 +1,35 @@
<script setup>
import { computed } from 'vue'
import { marked } from 'marked'
import FeatureBaum from './FeatureBaum.vue'
import ArchitekturTab from './ArchitekturTab.vue'
import FlowsTab from './FlowsTab.vue'
const props = defineProps({ projekt: { type: Object, required: true } })
// Lesemodus: Kern → Architektur → Bereiche in Abhängigkeitsordnung → Flows.
// Reine Sicht auf vorhandene Daten — keine eigenen Inhalte, kann nie veralten.
const bereicheGeordnet = computed(() => {
const nachName = Object.fromEntries(props.projekt.bereiche.map((b) => [b.name, b]))
return props.projekt.doku_ordnung.map((n) => nachName[n]).filter(Boolean)
})
const kernHtml = computed(() => marked.parse(props.projekt.kern.text || ''))
</script>
<template>
<article class="doku">
<h2>1. Kern</h2>
<div v-html="kernHtml"></div>
<h2>2. Architektur</h2>
<ArchitekturTab :projekt="projekt" />
<h2>3. Funktionsbereiche (in Einlese-Reihenfolge)</h2>
<FeatureBaum :projekt="{ ...projekt, bereiche: bereicheGeordnet }" />
<h2>4. Abläufe</h2>
<FlowsTab :projekt="projekt" />
</article>
</template>
<style scoped>
.doku h2 { font-size: 1.05rem; margin: 1.4rem 0 0.6rem; color: var(--dezent); }
.doku h2:first-child { margin-top: 0; }
</style>

View File

@@ -0,0 +1,72 @@
<script setup>
import { ref } from 'vue'
import { fetchEinheit } from '../api.js'
const props = defineProps({
projektName: { type: String, required: true },
id: { type: String, required: true },
kurz: { type: String, default: '' },
})
const detail = ref(null)
const fehler = ref('')
async function laden() {
if (detail.value || fehler.value) return // lazy: einmal beim ersten Aufklappen
try {
detail.value = await fetchEinheit(props.projektName, props.id)
} catch (e) {
fehler.value = e.message
}
}
</script>
<template>
<details class="einheit" @toggle="(e) => e.target.open && laden()">
<summary>
<code>{{ id }}</code>
<span v-if="detail?.veraltet" class="badge gelb">veraltet</span>
</summary>
<div class="inhalt">
<p v-if="fehler" class="fehler">{{ fehler }}</p>
<template v-else-if="detail">
<p class="besch"><em>{{ detail.beschreibung }}</em></p>
<dl class="io">
<dt>Input</dt><dd>{{ detail.input || '—' }}</dd>
<dt>Output</dt><dd>{{ detail.output || '—' }}</dd>
</dl>
<ul v-if="detail.entscheidungen.length" class="entscheidungen">
<li v-for="(e, i) in detail.entscheidungen" :key="i">{{ e }}</li>
</ul>
<p v-if="detail.kanten.length" class="kanten">
<template v-for="(k, i) in detail.kanten" :key="i">
<span v-if="i"> · </span>{{ k.typ }} <code>{{ k.ziel }}</code>
</template>
</p>
<pre v-if="detail.code">{{ detail.code }}</pre>
<p v-else class="kein-code">Anker nicht im aktuellen Code gefunden.</p>
</template>
<p v-else class="laden">lädt</p>
</div>
</details>
</template>
<style scoped>
.einheit {
margin: 0.3rem 0 0.5rem 1.1rem; border: 1px solid var(--rand); border-radius: 8px;
background: var(--bg);
}
.einheit > summary { padding: 0.4rem 0.6rem; font-size: 0.82rem; display: flex; gap: 0.5rem; }
.inhalt { padding: 0.1rem 0.8rem 0.7rem; font-size: 0.9rem; }
.besch { margin: 0.2rem 0; }
.io { display: grid; grid-template-columns: max-content 1fr; gap: 0.1rem 0.7rem;
color: var(--dezent); font-size: 0.85rem; margin: 0.3rem 0; }
.io dt { font-weight: 600; }
.io dd { margin: 0; }
.entscheidungen { margin: 0.3rem 0; padding-left: 1.2rem; }
.entscheidungen li { margin: 0.15rem 0; }
.kanten { color: var(--dezent); font-size: 0.82rem; }
.kein-code { color: var(--gelb); font-size: 0.85rem; }
.fehler { color: var(--rot); }
.laden { color: var(--dezent); }
</style>

View File

@@ -0,0 +1,60 @@
<script setup>
import { computed, ref } from 'vue'
// Geschäftsführungs-Sicht: WAS das Projekt kann — keine Einheiten, kein Code.
// EIN durchgehendes Karten-Raster über die volle Breite; der Bereich steht als
// Label auf der Karte. Klick vergrößert die Karte und zeigt alle Fähigkeiten.
const props = defineProps({ projekt: { type: Object, required: true } })
const karten = computed(() =>
props.projekt.bereiche.flatMap((b) =>
b.features.map((f) => ({ ...f, bereich: b.name, schluessel: b.name + f.titel }))))
const offen = ref(new Set())
function toggle(schluessel) {
offen.value.has(schluessel) ? offen.value.delete(schluessel) : offen.value.add(schluessel)
offen.value = new Set(offen.value)
}
</script>
<template>
<div class="raster">
<div v-for="f in karten" :key="f.schluessel" class="karte feature"
:class="{ offen: offen.has(f.schluessel) }" @click="toggle(f.schluessel)">
<span class="bereich">{{ f.bereich }}</span>
<div class="kopf">
<strong>{{ f.titel }}</strong>
</div>
<p class="besch">{{ f.beschreibung }}</p>
<ul v-if="offen.has(f.schluessel)" class="faehigkeiten">
<li v-for="tf in f.teilfeatures" :key="tf.titel">{{ tf.titel }}</li>
</ul>
<span v-else class="mehr">{{ f.teilfeatures.length }} Fähigkeiten </span>
</div>
</div>
</template>
<style scoped>
.raster {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 0.9rem;
align-items: start;
}
.feature { cursor: pointer; transition: border-color 0.1s; }
.feature:hover { border-color: var(--akzent); }
.feature.offen {
grid-column: span 2;
border-color: var(--akzent);
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.12);
}
.bereich {
display: block; font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.05em; color: var(--dezent); margin-bottom: 0.25rem;
}
.kopf { display: flex; align-items: baseline; gap: 0.6rem; }
.besch { color: var(--dezent); font-size: 0.9rem; margin: 0.3rem 0 0; }
.faehigkeiten { margin: 0.6rem 0 0; padding-left: 1.3rem; }
.faehigkeiten li { margin: 0.25rem 0; }
.mehr { display: block; margin-top: 0.5rem; color: var(--akzent); font-size: 0.82rem; }
</style>

View File

@@ -0,0 +1,33 @@
<script setup>
import EinheitDetail from './EinheitDetail.vue'
defineProps({ projekt: { type: Object, required: true } })
</script>
<template>
<section v-for="f in projekt.flows" :key="f.titel" class="flow">
<h3>{{ f.titel }}</h3>
<ol>
<li v-for="(s, i) in f.schritte" :key="i">
{{ s.text }}
<details v-if="s.einheiten.length" class="refs">
<summary><code v-for="(eid, j) in s.einheiten" :key="eid">{{ j ? ', ' : '' }}{{ eid }}</code></summary>
<EinheitDetail v-for="eid in s.einheiten" :key="eid"
:projekt-name="projekt.name" :id="eid" />
</details>
</li>
</ol>
</section>
<p v-if="!projekt.flows.length" class="leer">Keine Flow-Sicht vorhanden.</p>
</template>
<style scoped>
.flow {
background: var(--panel); border: 1px solid var(--rand); border-radius: 10px;
padding: 0.9rem 1.2rem; margin-bottom: 1rem;
}
.flow h3 { margin: 0 0 0.4rem; font-size: 1.02rem; }
.flow li { margin: 0.3rem 0; }
.refs > summary { font-size: 0.8rem; color: var(--dezent); padding: 0.15rem 0; }
.leer { color: var(--dezent); }
</style>

View File

@@ -0,0 +1,82 @@
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { marked } from 'marked'
import { fetchProjekt, fetchScanStatus } from '../api.js'
import { usePolling } from '../composables/usePolling.js'
import FeatureBaum from './FeatureBaum.vue'
import ArchitekturTab from './ArchitekturTab.vue'
import FlowsTab from './FlowsTab.vue'
import PruefungTab from './PruefungTab.vue'
import DokuTab from './DokuTab.vue'
import ScanSeitenleiste from './ScanSeitenleiste.vue'
const props = defineProps({
name: { type: String, required: true },
ansicht: { type: String, default: 'features' },
})
const projekt = ref(null)
const fehler = ref('')
const scanLaeuft = ref(false)
async function laden() {
try {
projekt.value = await fetchProjekt(props.name)
fehler.value = ''
} catch (e) {
fehler.value = e.message
}
}
async function statusTick() {
try {
const s = await fetchScanStatus(props.name)
const vorher = scanLaeuft.value
scanLaeuft.value = s.laeuft
if (vorher && !s.laeuft) await laden()
} catch { /* nie fatal */ }
}
const poll = usePolling(statusTick, () => scanLaeuft.value, 2000)
onMounted(async () => {
await laden()
await statusTick()
if (scanLaeuft.value) poll.start()
})
watch(() => props.name, async () => { projekt.value = null; await laden() })
const kernHtml = computed(() =>
projekt.value ? marked.parse(projekt.value.kern.text || '') : '')
</script>
<template>
<p v-if="fehler" class="fehler">{{ fehler }}</p>
<div v-else-if="projekt" class="rahmen" :class="{ mitLeiste: scanLaeuft }">
<div>
<section class="kern karte">
<h2>{{ projekt.kern.titel || name }}</h2>
<div class="kern-text" v-html="kernHtml"></div>
<p v-if="projekt.drift.veraltet.length" class="drift-hinweis">
{{ projekt.drift.veraltet.length }} Datei(en) seit dem letzten Scan geändert Re-Scan empfohlen.
</p>
</section>
<FeatureBaum v-if="ansicht === 'features'" :projekt="projekt" />
<ArchitekturTab v-else-if="ansicht === 'architektur'" :projekt="projekt" />
<FlowsTab v-else-if="ansicht === 'flows'" :projekt="projekt" />
<DokuTab v-else-if="ansicht === 'doku'" :projekt="projekt" />
<PruefungTab v-else :projekt="projekt" />
</div>
<ScanSeitenleiste v-if="scanLaeuft" :name="name" />
</div>
<p v-else class="laden">lädt</p>
</template>
<style scoped>
.rahmen.mitLeiste { display: grid; grid-template-columns: 1fr 300px; gap: 1.2rem; }
.kern { margin-bottom: 1.2rem; }
.kern h2 { margin: 0 0 0.4rem; font-size: 1.2rem; }
.kern-text :deep(p) { margin: 0.3rem 0; color: var(--dezent); }
.drift-hinweis { color: var(--gelb); font-size: 0.88rem; margin: 0.6rem 0 0; }
.fehler { color: var(--rot); }
.laden { color: var(--dezent); }
</style>

View File

@@ -0,0 +1,66 @@
<script setup>
import { ref } from 'vue'
import { starteScan } from '../api.js'
const props = defineProps({ projekt: { type: Object, required: true } })
const emit = defineEmits(['oeffne', 'aktualisiert'])
const meldung = ref('')
async function scan(art) {
meldung.value = ''
try {
await starteScan(props.projekt.name, art)
emit('aktualisiert')
} catch (e) {
meldung.value = e.message // 409: dirty tree, Rolle unkonfiguriert, …
}
}
function zeit(ts) {
if (!ts) return 'nie gescannt'
return new Date(ts).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short' })
}
</script>
<template>
<div class="karte" :class="{ tot: projekt.fehler }">
<div class="zeile">
<a v-if="!projekt.fehler && projekt.hat_planer" href="#" class="name"
@click.prevent="$emit('oeffne')">{{ projekt.name }}</a>
<span v-else class="name">{{ projekt.name }}</span>
<span v-if="projekt.scan_laeuft" class="badge gelb">läuft</span>
<span v-else-if="projekt.fehler" class="badge rot">Fehler</span>
<span v-else-if="!projekt.hat_planer" class="badge gelb">ungescannt</span>
<span v-else-if="projekt.drift?.length" class="badge gelb">Drift: {{ projekt.drift.length }}</span>
<span v-else class="badge gruen">aktuell</span>
</div>
<p class="detail" v-if="projekt.fehler">{{ projekt.fehler }}</p>
<p class="detail" v-else>
{{ projekt.pfad }} · letzter Scan: {{ zeit(projekt.letzter_scan_ts) }}
</p>
<p class="detail drift" v-if="projekt.drift?.length">veraltet: {{ projekt.drift.join(', ') }}</p>
<div class="aktionen" v-if="!projekt.fehler">
<button v-if="!projekt.hat_planer" class="primaer" :disabled="projekt.scan_laeuft"
@click="scan('scan')">Scannen</button>
<button v-else :disabled="projekt.scan_laeuft" @click="scan('rescan')">Re-Scan</button>
</div>
<p v-if="meldung" class="meldung">{{ meldung }}</p>
</div>
</template>
<style scoped>
.karte {
background: var(--panel);
border: 1px solid var(--rand);
border-radius: 10px;
padding: 1rem 1.2rem;
}
.karte.tot { opacity: 0.7; }
.zeile { display: flex; align-items: baseline; gap: 0.6rem; }
.name { font-weight: 600; color: var(--text); text-decoration: none; }
a.name:hover { color: var(--akzent); }
.detail { color: var(--dezent); font-size: 0.85rem; margin: 0.3rem 0 0; overflow-wrap: anywhere; }
.drift { color: var(--gelb); }
.aktionen { margin-top: 0.7rem; }
.meldung { color: var(--rot); font-size: 0.85rem; margin: 0.5rem 0 0; }
</style>

View File

@@ -0,0 +1,27 @@
<script setup>
defineProps({ projekt: { type: Object, required: true } })
</script>
<template>
<section class="pruefung">
<dl>
<dt>Tests</dt><dd><code>{{ projekt.pruefung.tests || '—' }}</code></dd>
<dt>Lint</dt><dd><code>{{ projekt.pruefung.lint || '—' }}</code></dd>
<dt>Start</dt><dd><code>{{ projekt.pruefung.start || '—' }}</code></dd>
</dl>
<ul v-if="projekt.pruefung.hinweise.length">
<li v-for="(h, i) in projekt.pruefung.hinweise" :key="i">{{ h }}</li>
</ul>
</section>
</template>
<style scoped>
.pruefung {
background: var(--panel); border: 1px solid var(--rand); border-radius: 10px;
padding: 0.9rem 1.2rem;
}
dl { display: grid; grid-template-columns: max-content 1fr; gap: 0.3rem 1rem; margin: 0; }
dt { font-weight: 600; }
dd { margin: 0; }
ul { color: var(--dezent); margin: 0.7rem 0 0; }
</style>

View File

@@ -0,0 +1,60 @@
<script setup>
import { onMounted, ref } from 'vue'
import { fetchScanStatus } from '../api.js'
import { usePolling } from '../composables/usePolling.js'
const props = defineProps({ name: { type: String, required: true } })
const status = ref(null)
async function tick() {
try {
status.value = await fetchScanStatus(props.name)
} catch { /* nie fatal */ }
}
const poll = usePolling(tick, () => status.value?.laeuft !== false, 2000)
onMounted(async () => {
await tick()
poll.start()
})
function dauer(s) {
return `${Math.round(s)}s`
}
</script>
<template>
<aside class="leiste">
<h3>Scan läuft</h3>
<template v-if="status">
<dl class="stufen">
<template v-for="(n, stufe) in status.stufen" :key="stufe">
<dt>{{ stufe }}</dt><dd>{{ n }}</dd>
</template>
</dl>
<div v-if="status.agenten.length" class="agenten">
<h4>Aktive Agenten</h4>
<p v-for="a in status.agenten" :key="a.key" class="agent">
<code>{{ a.label || a.key }}</code> · {{ dauer(a.runtime) }}
</p>
</div>
<p v-if="status.dead.length" class="dead">DEAD: {{ status.dead.join(', ') }}</p>
</template>
</aside>
</template>
<style scoped>
.leiste {
background: var(--panel); border: 1px solid var(--rand); border-radius: 10px;
padding: 0.9rem 1.1rem; align-self: start; position: sticky; top: 1rem;
}
.leiste h3 { margin: 0 0 0.5rem; font-size: 0.95rem; }
.leiste h4 { margin: 0.7rem 0 0.3rem; font-size: 0.8rem; color: var(--dezent);
text-transform: uppercase; letter-spacing: 0.04em; }
.stufen { display: grid; grid-template-columns: 1fr max-content; gap: 0.15rem 0.8rem;
font-size: 0.85rem; margin: 0; }
.stufen dt { color: var(--dezent); }
.stufen dd { margin: 0; text-align: right; }
.agent { font-size: 0.78rem; margin: 0.2rem 0; overflow-wrap: anywhere; }
.dead { color: var(--rot); font-size: 0.82rem; margin: 0.6rem 0 0; }
</style>

View File

@@ -0,0 +1,43 @@
<script setup>
import { onMounted, ref } from 'vue'
import { fetchProjekte } from '../api.js'
import { usePolling } from '../composables/usePolling.js'
import ProjektKarte from './ProjektKarte.vue'
defineEmits(['oeffne'])
const projekte = ref([])
const fehler = ref('')
async function laden() {
try {
projekte.value = await fetchProjekte()
fehler.value = ''
} catch (e) {
fehler.value = e.message
}
}
const poll = usePolling(laden, () => projekte.value.some((p) => p.scan_laeuft), 3000)
onMounted(async () => {
await laden()
poll.start()
})
</script>
<template>
<p v-if="fehler" class="fehler">{{ fehler }}</p>
<p v-else-if="!projekte.length" class="leer">
Keine Projekte Pfade in <code>projekte.txt</code> eintragen (eine Zeile = ein Repo).
</p>
<div class="liste">
<ProjektKarte v-for="p in projekte" :key="p.name" :projekt="p"
@oeffne="$emit('oeffne', p.name)" @aktualisiert="laden(); poll.start()" />
</div>
</template>
<style scoped>
.liste { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem; }
.fehler { color: var(--rot); }
.leer { color: var(--dezent); }
</style>