update
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import json
|
||||
import shutil
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
@@ -45,6 +46,38 @@ async def get_topics():
|
||||
return db_topics + sorted(derived - set(db_topics))
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_stats():
|
||||
"""Tracker: Themen-Anzahl + pro Format erstellt/absolviert (alle Kapitel abgehakt)."""
|
||||
guides = await list_guides()
|
||||
themen = set(await db_list_topics()) | {g["topic"] for g in guides} | set(bausteine_topics())
|
||||
if PROJECTS_DIR.is_dir():
|
||||
themen |= {e.name for e in PROJECTS_DIR.iterdir() if e.is_dir()}
|
||||
|
||||
formate = {}
|
||||
for fmt in ("MiniGuide", "Guide", "FullGuide"):
|
||||
# Pro Thema zählt nur der neueste fertige Guide (Altläufe teilen die Content-Datei)
|
||||
neueste: dict[str, dict] = {}
|
||||
for g in guides:
|
||||
if g["format"] == fmt and g["status"] == "done":
|
||||
if g["topic"] not in neueste or g["created_at"] > neueste[g["topic"]]["created_at"]:
|
||||
neueste[g["topic"]] = g
|
||||
absolviert = 0
|
||||
for g in neueste.values():
|
||||
path = guide_content_path(g["topic"], fmt)
|
||||
if not path.exists():
|
||||
continue
|
||||
try:
|
||||
chapters = json.loads(path.read_text(encoding="utf-8")).get("chapters", [])
|
||||
except ValueError:
|
||||
continue
|
||||
titles = {c.get("title") for c in chapters}
|
||||
if titles and titles <= set(await list_progress(g["id"])):
|
||||
absolviert += 1
|
||||
formate[fmt] = {"erstellt": len(neueste), "absolviert": absolviert}
|
||||
return {"themen": len(themen), "formate": formate}
|
||||
|
||||
|
||||
@router.post("/topics")
|
||||
async def add_topic(req: TopicCreateRequest):
|
||||
await create_topic(req.name.strip())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, computed, 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 } 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 } from './api.js'
|
||||
import TopicSidebar from './components/TopicSidebar.vue'
|
||||
import TopicDetail from './components/TopicDetail.vue'
|
||||
|
||||
@@ -21,6 +21,15 @@ const bausteine = ref({ ...EMPTY_BAUSTEINE })
|
||||
const activeBausteine = ref([])
|
||||
const provider = ref(localStorage.getItem('provider') || 'claude')
|
||||
const providers = ref([])
|
||||
const stats = ref(null)
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
stats.value = await fetchStats()
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Statistik:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function setProvider(id) {
|
||||
provider.value = id
|
||||
@@ -112,6 +121,7 @@ async function loadTopics() {
|
||||
async function loadGuides() {
|
||||
try {
|
||||
guides.value = await fetchGuides()
|
||||
loadStats()
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden:', e)
|
||||
}
|
||||
@@ -288,6 +298,7 @@ onUnmounted(() => {
|
||||
:topics="topics"
|
||||
:projects="projectNames"
|
||||
:selectedTopic="selectedTopic"
|
||||
:stats="stats"
|
||||
:doneByFormat="doneByFormat"
|
||||
:latestByFormat="latestByFormat"
|
||||
:allGuides="guides"
|
||||
@@ -318,6 +329,7 @@ onUnmounted(() => {
|
||||
:previewGuide="previewGuide"
|
||||
:dark="darkMode"
|
||||
:provider="provider"
|
||||
@progressChanged="loadStats"
|
||||
/>
|
||||
<div v-else class="empty-main">
|
||||
<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' })
|
||||
}
|
||||
|
||||
export async function fetchStats() {
|
||||
const res = await fetch(`${BASE}/stats`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function fetchProviders() {
|
||||
const res = await fetch(`${BASE}/providers`)
|
||||
return res.json()
|
||||
|
||||
@@ -28,6 +28,8 @@ const props = defineProps({
|
||||
provider: { type: String, default: 'claude' },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['progressChanged'])
|
||||
|
||||
const isOnePager = computed(() => props.previewGuide?.format === 'OnePager')
|
||||
|
||||
// --- Inhalt laden ---
|
||||
@@ -73,6 +75,7 @@ async function toggleChapter(title) {
|
||||
try {
|
||||
const res = await setProgress(props.previewGuide.id, title, newState)
|
||||
doneChapters.value = new Set(res.chapters || [])
|
||||
emit('progressChanged')
|
||||
} catch {
|
||||
const rollback = new Set(doneChapters.value)
|
||||
if (newState) rollback.delete(title)
|
||||
|
||||
@@ -5,6 +5,7 @@ const props = defineProps({
|
||||
topics: { type: Array, required: true },
|
||||
projects: { type: Array, default: () => [] },
|
||||
selectedTopic: { type: String, default: null },
|
||||
stats: { type: Object, default: null },
|
||||
doneByFormat: { type: Object, default: () => ({}) },
|
||||
latestByFormat: { type: Object, default: () => ({}) },
|
||||
allGuides: { type: Array, default: () => [] },
|
||||
@@ -25,6 +26,19 @@ function providerAvailable(id) {
|
||||
|
||||
const PROVIDER_LABELS = { claude: 'Claude', minimax: 'MiniMax', lokal: 'Lokal' }
|
||||
|
||||
// Tracker oben in der Navigation: Themen gesamt, pro Format erstellt/absolviert
|
||||
const trackerItems = computed(() => {
|
||||
if (!props.stats) return []
|
||||
const f = props.stats.formate || {}
|
||||
const fmt = (k) => `${f[k]?.absolviert ?? 0}/${f[k]?.erstellt ?? 0}`
|
||||
return [
|
||||
{ label: 'Themen', value: String(props.stats.themen ?? 0), title: 'Themen inkl. Projekte' },
|
||||
{ label: 'MiniGuides', value: fmt('MiniGuide'), title: 'absolviert/erstellt' },
|
||||
{ label: 'Guides', value: fmt('Guide'), title: 'absolviert/erstellt' },
|
||||
{ label: 'FullGuides', value: fmt('FullGuide'), title: 'absolviert/erstellt' },
|
||||
]
|
||||
})
|
||||
|
||||
const formats = [
|
||||
{ key: 'OnePager', label: 'OnePager' },
|
||||
{ key: 'MiniGuide', label: 'MiniGuide' },
|
||||
@@ -167,6 +181,12 @@ function confirmDeleteProject(name) {
|
||||
|
||||
<template>
|
||||
<aside class="sidebar" @mouseleave="emit('sidebarLeave')">
|
||||
<div class="stats-bar" v-if="trackerItems.length">
|
||||
<div class="stat" v-for="item in trackerItems" :key="item.label" :title="item.title">
|
||||
<span class="stat-value">{{ item.value }}</span>
|
||||
<span class="stat-label">{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="new-topic">
|
||||
<button
|
||||
class="pin-btn"
|
||||
@@ -407,6 +427,40 @@ function confirmDeleteProject(name) {
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
padding: 0.5rem 0.75rem;
|
||||
gap: 4px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.stat {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
padding: 4px 2px;
|
||||
background: var(--panel-soft);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.58rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--text-faint);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.provider-toggle {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
|
||||
Reference in New Issue
Block a user