update
This commit is contained in:
@@ -46,10 +46,31 @@ async def get_topics():
|
|||||||
return db_topics + sorted(derived - set(db_topics))
|
return db_topics + sorted(derived - set(db_topics))
|
||||||
|
|
||||||
|
|
||||||
# Lernschulden-Regel: JE Format (MiniGuide/Guide/FullGuide) höchstens 5 erstellte,
|
# Lernschulden-Regeln (nur Neu-Erstellungen; Themen, Bausteine, OnePager unbegrenzt):
|
||||||
# aber nicht absolvierte Guides. Darüber sind nur Neu-Generierungen bereits
|
# - JE Format (MiniGuide/Guide/FullGuide) höchstens 3 erstellte, nicht absolvierte Guides
|
||||||
# erstellter Guides erlaubt. Themen, Bausteine und OnePager sind unbegrenzt.
|
# - Progression pro Thema: Guide erst nach absolviertem MiniGuide, FullGuide erst nach absolviertem Guide
|
||||||
MAX_OFFENE_GUIDES = 5
|
MAX_OFFENE_GUIDES = 3
|
||||||
|
VORSTUFE = {"Guide": "MiniGuide", "FullGuide": "Guide"}
|
||||||
|
|
||||||
|
|
||||||
|
async def _ist_absolviert(topic: str, fmt: str) -> bool:
|
||||||
|
"""Alle Kapitel des neuesten fertigen Guides (Thema+Format) abgehakt?"""
|
||||||
|
neueste = None
|
||||||
|
for g in await list_guides():
|
||||||
|
if g["topic"] == topic and g["format"] == fmt and g["status"] == "done":
|
||||||
|
if neueste is None or g["created_at"] > neueste["created_at"]:
|
||||||
|
neueste = g
|
||||||
|
if neueste is None:
|
||||||
|
return False
|
||||||
|
path = guide_content_path(topic, fmt)
|
||||||
|
if not path.exists():
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
chapters = json.loads(path.read_text(encoding="utf-8")).get("chapters", [])
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
titles = {c.get("title") for c in chapters}
|
||||||
|
return bool(titles) and titles <= set(await list_progress(neueste["id"]))
|
||||||
|
|
||||||
|
|
||||||
async def _formate_stats() -> dict:
|
async def _formate_stats() -> dict:
|
||||||
@@ -88,6 +109,12 @@ async def get_stats():
|
|||||||
return {"themen": len(themen), "formate": await _formate_stats()}
|
return {"themen": len(themen), "formate": await _formate_stats()}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/topics/fortschritt")
|
||||||
|
async def topic_fortschritt(topic: str):
|
||||||
|
"""Absolviert-Status pro Format — fürs Freischalten der nächsten Ausbaustufe."""
|
||||||
|
return {fmt: await _ist_absolviert(topic, fmt) for fmt in ("MiniGuide", "Guide", "FullGuide")}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/topics")
|
@router.post("/topics")
|
||||||
async def add_topic(req: TopicCreateRequest):
|
async def add_topic(req: TopicCreateRequest):
|
||||||
await create_topic(req.name.strip())
|
await create_topic(req.name.strip())
|
||||||
@@ -169,10 +196,13 @@ async def create(req: GuideCreateRequest):
|
|||||||
for g in await list_guides():
|
for g in await list_guides():
|
||||||
if g["topic"] == req.topic.strip() and g["format"] == req.format and g["status"] in ("queued", "generating"):
|
if g["topic"] == req.topic.strip() and g["format"] == req.format and g["status"] in ("queued", "generating"):
|
||||||
raise HTTPException(409, "Generierung läuft bereits")
|
raise HTTPException(409, "Generierung läuft bereits")
|
||||||
# Lernschulden-Regel: neue Guides nur, wenn das Format weniger als 5 offene hat (erstellt, nicht absolviert).
|
# Lernschulden-Regeln — nur für Neu-Erstellungen; Resume (Schritt-Dateien
|
||||||
# Resume (Schritt-Dateien vorhanden) ist ausgenommen — der Guide wurde bereits angefangen.
|
# vorhanden) und Neu-Generieren bestehender Guides sind ausgenommen.
|
||||||
content = guide_content_path(req.topic.strip(), req.format)
|
content = guide_content_path(req.topic.strip(), req.format)
|
||||||
if req.format != "OnePager" and not content.exists() and not guide_slot_dateien(content):
|
if req.format != "OnePager" and not content.exists() and not guide_slot_dateien(content):
|
||||||
|
vorstufe = VORSTUFE.get(req.format)
|
||||||
|
if vorstufe and not await _ist_absolviert(req.topic.strip(), vorstufe):
|
||||||
|
raise HTTPException(409, f"Erst den {vorstufe} dieses Themas absolvieren")
|
||||||
stat = (await _formate_stats()).get(req.format, {"erstellt": 0, "absolviert": 0})
|
stat = (await _formate_stats()).get(req.format, {"erstellt": 0, "absolviert": 0})
|
||||||
offen = stat["erstellt"] - stat["absolviert"]
|
offen = stat["erstellt"] - stat["absolviert"]
|
||||||
if offen >= MAX_OFFENE_GUIDES:
|
if offen >= MAX_OFFENE_GUIDES:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
import { fetchGuides, fetchTopics, createTopic as apiCreateTopic, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, cancelBausteine as apiCancelBausteine, deleteBausteine as apiDeleteBausteine, fetchProjects, deleteProject as apiDeleteProject, fetchProviders, fetchStats } from './api.js'
|
import { fetchGuides, fetchTopics, createTopic as apiCreateTopic, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, cancelBausteine as apiCancelBausteine, deleteBausteine as apiDeleteBausteine, fetchProjects, deleteProject as apiDeleteProject, fetchProviders, fetchStats, fetchTopicFortschritt } from './api.js'
|
||||||
import TopicSidebar from './components/TopicSidebar.vue'
|
import TopicSidebar from './components/TopicSidebar.vue'
|
||||||
import TopicDetail from './components/TopicDetail.vue'
|
import TopicDetail from './components/TopicDetail.vue'
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ const activeBausteine = ref([])
|
|||||||
const provider = ref(localStorage.getItem('provider') || 'claude')
|
const provider = ref(localStorage.getItem('provider') || 'claude')
|
||||||
const providers = ref([])
|
const providers = ref([])
|
||||||
const stats = ref(null)
|
const stats = ref(null)
|
||||||
|
const fortschritt = ref({})
|
||||||
|
|
||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
try {
|
try {
|
||||||
@@ -145,8 +146,10 @@ async function loadBausteine() {
|
|||||||
activeBausteine.value = await fetchActiveBausteine()
|
activeBausteine.value = await fetchActiveBausteine()
|
||||||
if (selectedTopic.value) {
|
if (selectedTopic.value) {
|
||||||
bausteine.value = await fetchBausteineStatus(selectedTopic.value)
|
bausteine.value = await fetchBausteineStatus(selectedTopic.value)
|
||||||
|
fortschritt.value = await fetchTopicFortschritt(selectedTopic.value)
|
||||||
} else {
|
} else {
|
||||||
bausteine.value = { ...EMPTY_BAUSTEINE }
|
bausteine.value = { ...EMPTY_BAUSTEINE }
|
||||||
|
fortschritt.value = {}
|
||||||
}
|
}
|
||||||
if (activeBausteine.value.length && !pollTimer) startPolling()
|
if (activeBausteine.value.length && !pollTimer) startPolling()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -325,6 +328,7 @@ onUnmounted(() => {
|
|||||||
:projects="projectNames"
|
:projects="projectNames"
|
||||||
:selectedTopic="selectedTopic"
|
:selectedTopic="selectedTopic"
|
||||||
:stats="stats"
|
:stats="stats"
|
||||||
|
:fortschritt="fortschritt"
|
||||||
:doneByFormat="doneByFormat"
|
:doneByFormat="doneByFormat"
|
||||||
:latestByFormat="latestByFormat"
|
:latestByFormat="latestByFormat"
|
||||||
:allGuides="guides"
|
:allGuides="guides"
|
||||||
@@ -357,7 +361,7 @@ onUnmounted(() => {
|
|||||||
:previewGuide="previewGuide"
|
:previewGuide="previewGuide"
|
||||||
:dark="darkMode"
|
:dark="darkMode"
|
||||||
:provider="provider"
|
:provider="provider"
|
||||||
@progressChanged="loadStats"
|
@progressChanged="loadStats(); loadBausteine()"
|
||||||
/>
|
/>
|
||||||
<div v-else class="empty-main">
|
<div v-else class="empty-main">
|
||||||
<p>Thema in der Sidebar anlegen oder auswählen.</p>
|
<p>Thema in der Sidebar anlegen oder auswählen.</p>
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ export async function deleteBausteine(topic) {
|
|||||||
await fetch(`${BASE}/bausteine?topic=${encodeURIComponent(topic)}`, { method: 'DELETE' })
|
await fetch(`${BASE}/bausteine?topic=${encodeURIComponent(topic)}`, { method: 'DELETE' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchTopicFortschritt(topic) {
|
||||||
|
const res = await fetch(`${BASE}/topics/fortschritt?topic=${encodeURIComponent(topic)}`)
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchStats() {
|
export async function fetchStats() {
|
||||||
const res = await fetch(`${BASE}/stats`)
|
const res = await fetch(`${BASE}/stats`)
|
||||||
return res.json()
|
return res.json()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const props = defineProps({
|
|||||||
projects: { type: Array, default: () => [] },
|
projects: { type: Array, default: () => [] },
|
||||||
selectedTopic: { type: String, default: null },
|
selectedTopic: { type: String, default: null },
|
||||||
stats: { type: Object, default: null },
|
stats: { type: Object, default: null },
|
||||||
|
fortschritt: { type: Object, default: () => ({}) },
|
||||||
doneByFormat: { type: Object, default: () => ({}) },
|
doneByFormat: { type: Object, default: () => ({}) },
|
||||||
latestByFormat: { type: Object, default: () => ({}) },
|
latestByFormat: { type: Object, default: () => ({}) },
|
||||||
allGuides: { type: Array, default: () => [] },
|
allGuides: { type: Array, default: () => [] },
|
||||||
@@ -146,8 +147,10 @@ function handleFormatClick(format) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lernschulden-Regel: JE Format max. 5 erstellte, aber nicht absolvierte Guides —
|
// Lernschulden-Regeln (nur Neu-Erstellungen; Resume + Neu-Generieren bestehender erlaubt):
|
||||||
// darüber sind nur Neu-Generierungen bereits erstellter Guides erlaubt.
|
// Progression pro Thema (MiniGuide → Guide → FullGuide) + max. 3 offene je Format.
|
||||||
|
const VORSTUFE = { Guide: 'MiniGuide', FullGuide: 'Guide' }
|
||||||
|
|
||||||
function offeneGuides(format) {
|
function offeneGuides(format) {
|
||||||
const f = props.stats?.formate?.[format]
|
const f = props.stats?.formate?.[format]
|
||||||
return (f?.erstellt ?? 0) - (f?.absolviert ?? 0)
|
return (f?.erstellt ?? 0) - (f?.absolviert ?? 0)
|
||||||
@@ -156,9 +159,14 @@ function offeneGuides(format) {
|
|||||||
function playLock(format) {
|
function playLock(format) {
|
||||||
if (format === 'OnePager') return null
|
if (format === 'OnePager') return null
|
||||||
if (!props.bausteine.ready) return 'Erst Bausteine erstellen'
|
if (!props.bausteine.ready) return 'Erst Bausteine erstellen'
|
||||||
|
if (props.doneByFormat[format] || abgebrochen(format)) return null
|
||||||
|
const vorstufe = VORSTUFE[format]
|
||||||
|
if (vorstufe && !props.fortschritt?.[vorstufe]) {
|
||||||
|
return `Erst den ${vorstufe} dieses Themas absolvieren`
|
||||||
|
}
|
||||||
const offen = offeneGuides(format)
|
const offen = offeneGuides(format)
|
||||||
if (offen >= 5 && !props.doneByFormat[format]) {
|
if (offen >= 3) {
|
||||||
return `Erst ${format}s absolvieren — ${offen} offen (max. 5)`
|
return `Erst ${format}s absolvieren — ${offen} offen (max. 3)`
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user