Versionen je Thema

Ein Thema hat Versionen storage/<Thema>/v<N>/ mit eigenem Lauf, Buch und
Leseposition; bestehende Ablagen wandern beim Start nach v1/. Intern ist
„Thema/vN“ der Schlüssel aller Zustandsfunktionen. API: ?v=N an allen
Themen-Endpunkten, POST /version, DELETE ?v=N. Oberfläche: Versionsliste
unter dem Thema (Klick auf Thema → Lauf der neuesten, auf Version → Buch),
Knopf „Neue Version“ mit gleichen Eingaben. make loeschen kennt V=<Nr>.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
team3
2026-10-05 17:01:40 +02:00
parent 8d28680a12
commit 1b6899638b
10 changed files with 229 additions and 107 deletions

View File

@@ -4,7 +4,7 @@ import { api } from '../api.js'
import { render, zeit } from '../markdown.js'
import Lesemodus from './Lesemodus.vue'
const props = defineProps({ topic: String, laeuft: Boolean, fortschritt: Number })
const props = defineProps({ topic: String, version: Number, laeuft: Boolean, fortschritt: Number })
const daten = ref(null)
const lesemodus = ref(false)
// Vollbild nur aus der Klickgeste heraus (Browser-Vorgabe); iOS-Safari kennt die API nicht.
@@ -39,7 +39,7 @@ let ladeSeq = 0
let merkTimer = null
function merken(id) {
clearTimeout(merkTimer)
merkTimer = setTimeout(() => api.positionSetzen(props.topic, { abschnitt: id }).catch(() => {}), 1500)
merkTimer = setTimeout(() => api.positionSetzen(props.topic, { abschnitt: id }, props.version).catch(() => {}), 1500)
}
const cache = new Map() // Schlüssel: Abschnitt + Textlänge → unveränderte Abschnitte werden nicht neu gerendert
@@ -48,7 +48,7 @@ onUnmounted(() => { beobachter?.disconnect() })
async function laden() {
if (!props.topic) { daten.value = null; return }
const seq = ++ladeSeq
const [d, pos] = await Promise.all([api.guide(props.topic), api.position(props.topic).catch(() => ({}))])
const [d, pos] = await Promise.all([api.guide(props.topic, props.version), api.position(props.topic, props.version).catch(() => ({}))])
if (seq !== ladeSeq) return
daten.value = d
await nextTick()
@@ -57,7 +57,7 @@ async function laden() {
const ziel = location.hash.slice(1) || pos.abschnitt
if (ziel && !aktiv.value) document.getElementById(ziel)?.scrollIntoView({ block: 'start' })
}
watch(() => props.topic, () => { aktiv.value = ''; cache.clear(); laden() }, { immediate: true })
watch(() => [props.topic, props.version], () => { aktiv.value = ''; cache.clear(); laden() }, { immediate: true })
watch(() => props.laeuft, (l, alt) => { if (alt && !l) laden() })
// Während eines Laufs nur nachladen, wenn sich der Fortschritt geändert hat
watch(() => props.fortschritt, (n, alt) => { if (props.laeuft && n !== alt) laden() })
@@ -116,8 +116,8 @@ onUnmounted(() => window.removeEventListener('keydown', taste))
<button class="schliessen sekundaer klein" @click="navAuf = false">✕ Schließen</button>
<div class="werkzeuge">
<button class="klein" title="Lesemodus: Abschnitt für Abschnitt" @click="lesemodusStart" :disabled="!abschnitte.length">Lesen ▶</button>
<a class="knopf klein sekundaer" title="Als Markdown-Datei herunterladen" :href="api.guideMdUrl(topic)" download>MD</a>
<a v-if="daten?.pdf" class="knopf klein sekundaer" title="Als PDF herunterladen" :href="api.pdfUrl(topic)" download>PDF</a>
<a class="knopf klein sekundaer" title="Als Markdown-Datei herunterladen" :href="api.guideMdUrl(topic, version)" download>MD</a>
<a v-if="daten?.pdf" class="knopf klein sekundaer" title="Als PDF herunterladen" :href="api.pdfUrl(topic, version)" download>PDF</a>
</div>
<template v-if="daten">
<div class="meta">
@@ -142,8 +142,8 @@ onUnmounted(() => window.removeEventListener('keydown', taste))
<div class="mobil-leiste">
<button class="sekundaer klein" @click="navAuf = !navAuf">☰ Inhalt</button>
<button class="klein" @click="lesemodusStart" :disabled="!abschnitte.length">Lesen ▶</button>
<a class="knopf klein sekundaer" :href="api.guideMdUrl(topic)" download>MD</a>
<a v-if="daten?.pdf" class="knopf klein sekundaer" :href="api.pdfUrl(topic)" download>PDF</a>
<a class="knopf klein sekundaer" :href="api.guideMdUrl(topic, version)" download>MD</a>
<a v-if="daten?.pdf" class="knopf klein sekundaer" :href="api.pdfUrl(topic, version)" download>PDF</a>
</div>
<article v-if="daten" class="buch">
<h1>{{ daten.titel }}</h1>
@@ -190,6 +190,6 @@ onUnmounted(() => window.removeEventListener('keydown', taste))
</article>
</div>
<Lesemodus v-if="lesemodus && daten" :daten="daten" :topic="topic" @schliessen="lesemodusEnde" />
<Lesemodus v-if="lesemodus && daten" :daten="daten" :topic="topic" :version="version" @schliessen="lesemodusEnde" />
</div>
</template>

View File

@@ -3,7 +3,7 @@ import { computed } from 'vue'
import { render } from '../markdown.js'
const props = defineProps({ state: Object })
const emit = defineEmits(['start', 'pause', 'reset'])
const emit = defineEmits(['start', 'pause', 'reset', 'neue-version'])
const laeuft = computed(() => props.state?.status === 'laeuft')
const fertig = computed(() => props.state?.status === 'fertig')
@@ -36,6 +36,7 @@ const gruppen = computed(() => {
const ereignisse = computed(() => (props.state?.ereignisse || []).slice(-100).reverse())
function reset() { if (confirm('Lauf zurücksetzen? Alles außer dem Thema wird gelöscht.')) emit('reset') }
function neueVersion() { if (confirm('Neue Version erzeugen? Gleiche Eingaben, kompletter neuer Lauf. Die bisherige Version bleibt erhalten.')) emit('neue-version') }
</script>
<template>
@@ -44,6 +45,7 @@ function reset() { if (confirm('Lauf zurücksetzen? Alles außer dem Thema wird
<button v-if="!laeuft && !fertig" @click="emit('start')">{{ begonnen ? 'Fortsetzen' : 'Buch erzeugen' }}</button>
<button v-if="laeuft" class="sekundaer" @click="emit('pause')">Pause</button>
<button v-if="!laeuft && begonnen && !fertig" class="sekundaer" @click="reset">Zurücksetzen</button>
<button v-if="!laeuft && begonnen" class="sekundaer" title="Gleiches Thema, gleiche Hinweise, neuer Lauf" @click="neueVersion">Neue Version</button>
<span class="spacer"></span>
<span class="badge" :class="statusFarbe[state.status]">{{ statusText[state.status] || state.status }}<template v-if="state.stufe && laeuft"> · {{ state.stufe }}</template></span>
<span class="badge">{{ usd(state.kosten_usd) }}</span>

View File

@@ -3,7 +3,7 @@ import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { render, zeit } from '../markdown.js'
import { api } from '../api.js'
const props = defineProps({ daten: Object, topic: String })
const props = defineProps({ daten: Object, topic: String, version: Number })
const emit = defineEmits(['schliessen'])
const buehne = ref(null)
const pos = ref(0)
@@ -53,7 +53,7 @@ watch(pos, (p) => {
buehne.value?.scrollTo({ top: 0 })
clearTimeout(merkTimer)
const key = screens.value[p]?.key
if (key) merkTimer = setTimeout(() => api.positionSetzen(props.topic, { lesemodus: key }).catch(() => {}), 1500)
if (key) merkTimer = setTimeout(() => api.positionSetzen(props.topic, { lesemodus: key }, props.version).catch(() => {}), 1500)
})
function taste(e) {
@@ -82,7 +82,7 @@ function wischEnde(e) {
}
onMounted(async () => {
window.addEventListener('keydown', taste)
const merk = (await api.position(props.topic).catch(() => ({}))).lesemodus
const merk = (await api.position(props.topic, props.version).catch(() => ({}))).lesemodus
// Alte Positionen waren ein Index, neue sind ein key.
const i = typeof merk === 'string' ? screens.value.findIndex((s) => s.key === merk) : Number(merk || 0)
pos.value = Math.min(Math.max(i, 0), screens.value.length - 1)