This commit is contained in:
team3
2026-07-23 11:54:31 +02:00
parent f07ef9653d
commit 09f9ea74a6
18 changed files with 364 additions and 85 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

64
frontend/dist/assets/index-PAbaEjVj.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -13,8 +13,8 @@
if (dark) document.documentElement.classList.add('dark')
})()
</script>
<script type="module" crossorigin src="/assets/index-DIiv29cS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-AxkPXHNh.css">
<script type="module" crossorigin src="/assets/index-PAbaEjVj.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CzKrDvqu.css">
</head>
<body>
<div id="app"></div>

View File

@@ -4,10 +4,19 @@ import { fetchProjekte } from './api.js'
import { usePolling } from './composables/usePolling.js'
import StartSeite from './components/StartSeite.vue'
import ProjektAnsicht from './components/ProjektAnsicht.vue'
import OrdnerBrowser from './components/OrdnerBrowser.vue'
const projekte = ref([])
const aktivesProjekt = ref(null) // null = Übersicht
const ansicht = ref('features')
const zeigeBrowser = ref(false)
const listeVersion = ref(0) // remountet StartSeite nach Hinzufügen
function hinzugefuegt() {
zeigeBrowser.value = false
listeVersion.value++
laden()
}
const ANSICHTEN = [
['features', 'Features', '▦'],
@@ -55,6 +64,7 @@ function toggleDark() {
<span v-else-if="p.drift?.length" class="punkt gelb" title="Drift"></span>
<span v-else-if="p.hat_planer" class="punkt gruen" title="aktuell"></span>
</a>
<a href="#" class="hinzu" @click.prevent="zeigeBrowser = true"> Projekt hinzufügen</a>
</nav>
<nav v-if="aktivesProjekt" class="ansichten">
<span class="gruppe">{{ aktivesProjekt }}</span>
@@ -66,9 +76,11 @@ function toggleDark() {
<button class="dark-knopf" @click="toggleDark">{{ darkMode ? ' Hell' : ' Dunkel' }}</button>
</aside>
<main>
<StartSeite v-if="!aktivesProjekt" @oeffne="oeffne" />
<StartSeite v-if="!aktivesProjekt" :key="listeVersion" @oeffne="oeffne" @geaendert="laden" />
<ProjektAnsicht v-else :name="aktivesProjekt" :ansicht="ansicht" />
</main>
<OrdnerBrowser v-if="zeigeBrowser" @schliessen="zeigeBrowser = false"
@hinzugefuegt="hinzugefuegt" />
</div>
</template>
@@ -93,6 +105,8 @@ nav a {
nav a:hover { background: var(--bg); }
nav a.aktiv { background: var(--akzent); color: var(--akzent-text); }
nav a.gestoert { color: var(--dezent); cursor: default; }
nav a.hinzu { color: var(--dezent); font-size: 0.85rem; }
nav a.hinzu:hover { color: var(--text); }
.icon { width: 1.1rem; text-align: center; color: inherit; opacity: 0.75; }
.punkt { width: 8px; height: 8px; border-radius: 99px; margin-left: auto; }
.punkt.gruen { background: var(--gruen); }

View File

@@ -34,6 +34,11 @@ async function req(path, { method = 'GET', body, query } = {}) {
}
export const fetchProjekte = () => req('/projekte')
export const fetchOrdner = (pfad) => req('/fs', { query: { pfad } })
export const projektHinzufuegen = (pfad) =>
req('/projekte', { method: 'POST', body: { pfad } })
export const projektEntfernen = (name) =>
req(`/projekte/${encodeURIComponent(name)}`, { method: 'DELETE' })
export const fetchProjekt = (name) => req(`/projekte/${encodeURIComponent(name)}`)
export const fetchEinheit = (name, id) =>
req(`/projekte/${encodeURIComponent(name)}/einheit`, { query: { id } })

View File

@@ -0,0 +1,94 @@
<script setup>
import { onMounted, ref } from 'vue'
import { fetchOrdner, projektHinzufuegen } from '../api.js'
const emit = defineEmits(['schliessen', 'hinzugefuegt'])
const pfad = ref('')
const eingabe = ref('')
const eltern = ref(null)
const ordner = ref([])
const fehler = ref('')
async function lade(ziel) {
try {
const d = await fetchOrdner(ziel)
pfad.value = d.pfad
eingabe.value = d.pfad
eltern.value = d.eltern
ordner.value = d.ordner
fehler.value = ''
} catch (e) {
fehler.value = e.message
}
}
async function uebernehmen() {
try {
await projektHinzufuegen(pfad.value)
emit('hinzugefuegt')
} catch (e) {
fehler.value = e.message
}
}
onMounted(() => lade(''))
</script>
<template>
<div class="schleier" @click.self="$emit('schliessen')" @keydown.esc="$emit('schliessen')">
<div class="dialog">
<div class="kopf">
<h3>Projekt-Ordner wählen</h3>
<button class="zu" @click="$emit('schliessen')"></button>
</div>
<input v-model="eingabe" spellcheck="false"
@keydown.enter="lade(eingabe)" title="Pfad eintippen, Enter lädt" />
<div class="liste">
<a v-if="eltern" href="#" class="eintrag" @click.prevent="lade(eltern)"> ..</a>
<a v-for="o in ordner" :key="o.pfad" href="#" class="eintrag"
@click.prevent="lade(o.pfad)">
<span class="ordner-icon"></span>{{ o.name }}
<span v-if="o.hat_planer" class="badge gruen">.planer</span>
</a>
<p v-if="!ordner.length" class="leer">keine Unterordner</p>
</div>
<p v-if="fehler" class="fehler">{{ fehler }}</p>
<div class="fuss">
<code class="gewaehlt">{{ pfad }}</code>
<button class="primaer" @click="uebernehmen">Als Projekt übernehmen</button>
</div>
</div>
</div>
</template>
<style scoped>
.schleier {
position: fixed; inset: 0; background: rgb(0 0 0 / 0.4); z-index: 50;
display: flex; align-items: center; justify-content: center;
}
.dialog {
background: var(--panel); border: 1px solid var(--rand); border-radius: 12px;
width: min(560px, 92vw); max-height: 80vh; padding: 1rem 1.2rem;
display: flex; flex-direction: column; gap: 0.7rem;
}
.kopf { display: flex; align-items: center; }
.kopf h3 { margin: 0; font-size: 1rem; flex: 1; }
.zu { border: none; background: none; color: var(--dezent); padding: 0.2rem; }
input {
font: inherit; color: var(--text); background: var(--bg);
border: 1px solid var(--rand); border-radius: 6px; padding: 0.4rem 0.6rem;
}
.liste { overflow-y: auto; min-height: 8rem; display: flex; flex-direction: column; }
.eintrag {
display: flex; align-items: center; gap: 0.45rem;
color: var(--text); text-decoration: none; font-size: 0.92rem;
padding: 0.3rem 0.5rem; border-radius: 6px;
}
.eintrag:hover { background: var(--bg); }
.ordner-icon { color: var(--dezent); }
.leer { color: var(--dezent); font-size: 0.85rem; margin: 0.3rem 0.5rem; }
.fehler { color: var(--rot); font-size: 0.85rem; margin: 0; }
.fuss { display: flex; align-items: center; gap: 0.8rem; border-top: 1px solid var(--rand); padding-top: 0.7rem; }
.gewaehlt { flex: 1; font-size: 0.8rem; overflow-wrap: anywhere; color: var(--dezent); }
</style>

View File

@@ -1,6 +1,6 @@
<script setup>
import { ref } from 'vue'
import { starteScan } from '../api.js'
import { projektEntfernen, starteScan } from '../api.js'
const props = defineProps({ projekt: { type: Object, required: true } })
const emit = defineEmits(['oeffne', 'aktualisiert'])
@@ -16,6 +16,19 @@ async function scan(art) {
}
}
async function entfernen() {
const ok = confirm(
`${props.projekt.name}" aus der Projektliste entfernen?\n.planer/ im Repo bleibt erhalten.`)
if (!ok) return
meldung.value = ''
try {
await projektEntfernen(props.projekt.name)
emit('aktualisiert')
} catch (e) {
meldung.value = e.message
}
}
function zeit(ts) {
if (!ts) return 'nie gescannt'
return new Date(ts).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short' })
@@ -39,10 +52,14 @@ function zeit(ts) {
{{ 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 class="aktionen">
<template 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>
</template>
<button class="entfernen" :disabled="projekt.scan_laeuft" @click="entfernen"
title=".planer/ im Repo bleibt erhalten">Entfernen</button>
</div>
<p v-if="meldung" class="meldung">{{ meldung }}</p>
</div>
@@ -61,6 +78,8 @@ function zeit(ts) {
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; }
.aktionen { margin-top: 0.7rem; display: flex; gap: 0.5rem; }
.entfernen { margin-left: auto; color: var(--dezent); }
.entfernen:hover:not(:disabled) { color: var(--rot); border-color: var(--rot); }
.meldung { color: var(--rot); font-size: 0.85rem; margin: 0.5rem 0 0; }
</style>

View File

@@ -4,7 +4,7 @@ import { fetchProjekte } from '../api.js'
import { usePolling } from '../composables/usePolling.js'
import ProjektKarte from './ProjektKarte.vue'
defineEmits(['oeffne'])
defineEmits(['oeffne', 'geaendert'])
const projekte = ref([])
const fehler = ref('')
@@ -32,7 +32,8 @@ onMounted(async () => {
</p>
<div class="liste">
<ProjektKarte v-for="p in projekte" :key="p.name" :projekt="p"
@oeffne="$emit('oeffne', p.name)" @aktualisiert="laden(); poll.start()" />
@oeffne="$emit('oeffne', p.name)"
@aktualisiert="laden(); poll.start(); $emit('geaendert')" />
</div>
</template>