from fastapi import APIRouter, Depends, Request, HTTPException from fastapi.responses import HTMLResponse from pymysql.cursors import DictCursor import json from collections import Counter from typing import List, Dict, Any, Optional from deps import get_db, current_user from services.config_loader import ConfigLoader router = APIRouter() @router.get("/crumbforest/pulse", response_class=HTMLResponse) async def pulse_dashboard(req: Request, tag: Optional[str] = None, user = Depends(current_user)): """ Show the Pulse (Blog) dashboard with tag cloud. """ # 1. Auth & Config if not user: # Public access allowed? User said "Neuigkeiten...". # But usually Crumbforest is internal. # Let's assume login required for now to access specific group templates. lang = req.query_params.get("lang", "de") return req.app.state.render(req, "pages/login.html", error="Login required for Pulse") # Actually Redirect is better # return RedirectResponse(f"/{lang}/login", status_code=302) user_group = user.get('user_group', 'demo') config = ConfigLoader.load_config() group_config = config.get('groups', {}).get(user_group, {}) lang = req.query_params.get("lang") or req.session.get("lang") or "de" req.session["lang"] = lang # 2. Fetch Posts conn = get_db() try: with conn.cursor() as cur: # Fetch all published posts base_query = """ SELECT id, title, slug, excerpt, tags, author, created_at FROM posts WHERE is_published = 1 """ cur.execute(base_query) posts_raw = cur.fetchall() finally: conn.close() # 3. Process Tags & Filter posts = [] all_tags = [] for p in posts_raw: # Parse JSON tags if isinstance(p['tags'], str): p['tags'] = json.loads(p['tags']) elif p['tags'] is None: p['tags'] = [] all_tags.extend(p['tags']) # Filter (if tag selected) if tag and tag not in p['tags']: continue posts.append(p) # 4. Build Tag Cloud # Count tags tag_counts = Counter(all_tags) # Convert to list of dicts for template: [{'name': 'RAG', 'count': 5, 'size': '...'}, ...] # Simple sizing logic: 1..10 tag_cloud = [] if tag_counts: max_count = max(tag_counts.values()) min_count = min(tag_counts.values()) delta = max_count - min_count or 1 for t_name, count in tag_counts.items(): # Linear scaling 1.0 to 2.0em size = 0.8 + (1.2 * (count - min_count) / delta) tag_cloud.append({ 'name': t_name, 'count': count, 'size': f"{size:.1f}em", 'active': t_name == tag }) # Sort cloud alphabetically tag_cloud.sort(key=lambda x: x['name']) return req.app.state.render( req, "crumbforest/blog.html", posts=posts, tag_cloud=tag_cloud, current_tag=tag, user_group=user_group, group_config=group_config, lang=lang ) @router.get("/crumbforest/pulse/{slug}", response_class=HTMLResponse) async def pulse_post(req: Request, slug: str, user = Depends(current_user)): """ Show a single blog post. """ if not user: # Redirect lang = req.query_params.get("lang", "de") return req.app.state.render(req, "pages/login.html") # Simplified user_group = user.get('user_group', 'demo') config = ConfigLoader.load_config() group_config = config.get('groups', {}).get(user_group, {}) lang = req.query_params.get("lang") or req.session.get("lang") or "de" conn = get_db() try: with conn.cursor() as cur: cur.execute( """ SELECT id, title, slug, excerpt, tags, author, body_md, created_at, updated_at FROM posts WHERE slug=%s AND is_published=1 """, (slug,) ) post = cur.fetchone() finally: conn.close() if not post: raise HTTPException(status_code=404, detail="Post not found") # Parse tags if isinstance(post['tags'], str): post['tags'] = json.loads(post['tags']) elif post['tags'] is None: post['tags'] = [] # Markdown rendering handled in template via filter? # Or pre-render here. # Usually we pass markdown and let template render it if using a JS lib, # OR we use `markdown` lib here. # checking existing templates... we likely use `markdown` filter or lib. # Code Tour mentioned "body_md". # I will assume we render MD in Python for safety/consistency? # Or use a client-side renderer (Zero-MD, Marked)? # Looking at `diary_rag.py` / `rag_service.py` -> we just store MD. # If I verify `app/templates/`? # I'll use `markdown` python lib if available. # Assume it is available or basic text for now. import markdown post['html_content'] = markdown.markdown(post['body_md']) return req.app.state.render( req, "crumbforest/post.html", post=post, group_config=group_config, lang=lang )