Tagebuch:
- Day-One-Listenansicht: Wochentag + Tageszahl + Meta-Zeile (Zeit/Ort/Wetter)
- 4 Ansichten: Liste, Medien-Mosaik, Kalender (mit Sprungbuttons), Karte (GPS-Marker)
- Detail-Ansicht inline im Content-Bereich (kein Fullscreen-Overlay mehr)
- Hero-Bild vollständig sichtbar (object-fit:contain), Lightbox mit Safe-Area
- 2-Spalten-Layout Desktop: Text + Leaflet-Karte + POI-Liste
- EXIF-GPS-Extraktion bei Foto-Upload, historisches Wetter via Archive-API
- NoteStation-Import: Fotos in diary_media (80 Einträge migriert, 94 Medien)
- Stats-Endpoints: /diary/stats, /diary/calendar, /diary/locations
Notiz-Feature:
- Generische notes-Tabelle (parent_type + parent_id + meta_json)
- 📝-Button in 8 Bereichen, Notizblock-Seite mit KI-Analyse
- KI-Toggle in Einstellungen, notes_ki_enabled in User-Profil
Icons & Design:
- fill:currentColor Fix für welcome/onboarding/friends.js
- --c-icon Variable, --c-text-muted Dark Mode aufgehellt
- 15+ neue Phosphor-Icons aus lokaler Kopie
- CSS Network-First im SW, Cache-Control-Middleware
Infrastruktur:
- Wiki-Anreicherungs-Scheduler-Jobs entfernt (abgeschlossen)
- auth.py: notes_ki_enabled + is_social_media im User-Response
263 lines
9.4 KiB
Python
263 lines
9.4 KiB
Python
"""BAN YARO — Notizen Routes"""
|
|
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
from typing import Optional, Any, List
|
|
from database import db
|
|
from auth import get_current_user
|
|
|
|
router = APIRouter()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Schemas
|
|
# ------------------------------------------------------------------
|
|
class NoteCreate(BaseModel):
|
|
text: str
|
|
meta_json: Optional[Any] = None
|
|
location_name: Optional[str] = None
|
|
parent_label: Optional[str] = None
|
|
|
|
|
|
class NoteUpdate(BaseModel):
|
|
text: Optional[str] = None
|
|
meta_json: Optional[Any] = None
|
|
location_name: Optional[str] = None
|
|
parent_label: Optional[str] = None
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Hilfsfunktionen
|
|
# ------------------------------------------------------------------
|
|
def _serialize(row) -> dict:
|
|
d = dict(row)
|
|
if d.get("meta_json") and isinstance(d["meta_json"], str):
|
|
try:
|
|
d["meta_json"] = json.loads(d["meta_json"])
|
|
except Exception:
|
|
pass
|
|
return d
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/notes — Gesamt-Notizblock mit Filtern
|
|
# Alias: GET /api/notes/all/0 (Rückwärtskompatibilität)
|
|
# WICHTIG: Diese Route muss VOR /{parent_type}/{parent_id} stehen!
|
|
# ------------------------------------------------------------------
|
|
@router.get("")
|
|
async def list_all_notes_filtered(
|
|
parent_type: Optional[List[str]] = Query(default=None),
|
|
date_from: Optional[str] = Query(default=None),
|
|
date_to: Optional[str] = Query(default=None),
|
|
q: Optional[str] = Query(default=None),
|
|
sort: Optional[str] = Query(default="date_desc"),
|
|
user=Depends(get_current_user),
|
|
):
|
|
"""Alle Notizen des Users mit optionalen Filtern."""
|
|
conditions = ["user_id=?"]
|
|
params: list = [user["id"]]
|
|
|
|
if parent_type:
|
|
placeholders = ",".join("?" * len(parent_type))
|
|
conditions.append(f"parent_type IN ({placeholders})")
|
|
params.extend(parent_type)
|
|
|
|
if date_from:
|
|
conditions.append("DATE(created_at) >= ?")
|
|
params.append(date_from)
|
|
|
|
if date_to:
|
|
conditions.append("DATE(created_at) <= ?")
|
|
params.append(date_to)
|
|
|
|
if q:
|
|
conditions.append("(text LIKE ? OR COALESCE(parent_label,'') LIKE ?)")
|
|
like = f"%{q}%"
|
|
params.extend([like, like])
|
|
|
|
where = " AND ".join(conditions)
|
|
|
|
if sort == "rubrik":
|
|
order = "parent_type ASC, created_at DESC"
|
|
elif sort == "ort":
|
|
order = "CASE WHEN location_name IS NULL OR location_name='' THEN 1 ELSE 0 END ASC, location_name ASC, created_at DESC"
|
|
elif sort == "date_asc":
|
|
order = "created_at ASC"
|
|
else:
|
|
order = "created_at DESC"
|
|
|
|
with db() as conn:
|
|
rows = conn.execute(
|
|
f"SELECT * FROM notes WHERE {where} ORDER BY {order}",
|
|
params
|
|
).fetchall()
|
|
|
|
return [_serialize(r) for r in rows]
|
|
|
|
|
|
@router.get("/all/0")
|
|
async def list_all_notes(user=Depends(get_current_user)):
|
|
"""Alias für Rückwärtskompatibilität."""
|
|
with db() as conn:
|
|
rows = conn.execute(
|
|
"SELECT * FROM notes WHERE user_id=? ORDER BY created_at DESC",
|
|
(user["id"],)
|
|
).fetchall()
|
|
return [_serialize(r) for r in rows]
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/notes/ki-analyse
|
|
# WICHTIG: Fixe Route MUSS vor /{parent_type}/{parent_id} stehen!
|
|
# ------------------------------------------------------------------
|
|
@router.post("/ki-analyse")
|
|
async def ki_analyse(user=Depends(get_current_user)):
|
|
"""KI analysiert die Notizen des Users und gibt Muster/Vorschläge zurück."""
|
|
with db() as conn:
|
|
# User-Setting prüfen
|
|
setting = conn.execute(
|
|
"SELECT notes_ki_enabled FROM users WHERE id=?",
|
|
(user["id"],)
|
|
).fetchone()
|
|
|
|
if not setting or not setting["notes_ki_enabled"]:
|
|
raise HTTPException(403, "KI-Assistent ist deaktiviert.")
|
|
|
|
with db() as conn:
|
|
rows = conn.execute(
|
|
"""SELECT text, parent_type, parent_label, location_name, created_at
|
|
FROM notes
|
|
WHERE user_id=?
|
|
ORDER BY created_at DESC
|
|
LIMIT 50""",
|
|
(user["id"],)
|
|
).fetchall()
|
|
|
|
note_count = len(rows)
|
|
if note_count == 0:
|
|
return {"suggestions": "", "note_count": 0}
|
|
|
|
notes_data = [dict(r) for r in rows]
|
|
|
|
prompt = (
|
|
"Du bist ein freundlicher Assistent für Hundebesitzer. "
|
|
"Analysiere diese Notizen und erkenne Muster (Gesundheit, Training, Verhalten, "
|
|
"Lieblingsrouten, saisonale Besonderheiten). "
|
|
"Gib 2-4 kurze, konkrete Vorschläge auf Deutsch. "
|
|
"Keine langen Texte, bullet points. "
|
|
f"Daten: {json.dumps(notes_data, ensure_ascii=False)}"
|
|
)
|
|
|
|
try:
|
|
import ki as ki_module
|
|
suggestions, _ = await ki_module.complete(
|
|
prompt,
|
|
requires_premium=False,
|
|
user_is_premium=False,
|
|
)
|
|
except Exception as e:
|
|
logger.warning("KI-Analyse fehlgeschlagen: %s", e)
|
|
suggestions = ""
|
|
|
|
return {"suggestions": suggestions, "note_count": note_count}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/notes/{parent_type}/{parent_id}
|
|
# parent_id kann ein Integer oder ein String-Schlüssel sein.
|
|
# SQLite ist dynamisch getypt — wir übergeben den Wert als Text.
|
|
# ------------------------------------------------------------------
|
|
@router.get("/{parent_type}/{parent_id}")
|
|
async def list_notes(parent_type: str, parent_id: str,
|
|
user=Depends(get_current_user)):
|
|
with db() as conn:
|
|
rows = conn.execute(
|
|
"""SELECT * FROM notes
|
|
WHERE user_id=? AND parent_type=? AND CAST(parent_id AS TEXT)=?
|
|
ORDER BY created_at DESC""",
|
|
(user["id"], parent_type, parent_id)
|
|
).fetchall()
|
|
return [_serialize(r) for r in rows]
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/notes/{parent_type}/{parent_id}
|
|
# ------------------------------------------------------------------
|
|
@router.post("/{parent_type}/{parent_id}", status_code=201)
|
|
async def create_note(parent_type: str, parent_id: str, data: NoteCreate,
|
|
user=Depends(get_current_user)):
|
|
if not data.text.strip():
|
|
raise HTTPException(400, "Notiz darf nicht leer sein.")
|
|
|
|
meta_str = json.dumps(data.meta_json) if data.meta_json is not None else None
|
|
now = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
with db() as conn:
|
|
conn.execute(
|
|
"""INSERT INTO notes
|
|
(user_id, parent_type, parent_id, text, meta_json,
|
|
location_name, parent_label, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(user["id"], parent_type, parent_id, data.text.strip(), meta_str,
|
|
data.location_name, data.parent_label, now, now)
|
|
)
|
|
row = conn.execute(
|
|
"SELECT * FROM notes WHERE user_id=? AND parent_type=? AND parent_id=? ORDER BY id DESC LIMIT 1",
|
|
(user["id"], parent_type, parent_id)
|
|
).fetchone()
|
|
return _serialize(row)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# PATCH /api/notes/{id}
|
|
# ------------------------------------------------------------------
|
|
@router.patch("/{note_id}")
|
|
async def update_note(note_id: int, data: NoteUpdate,
|
|
user=Depends(get_current_user)):
|
|
with db() as conn:
|
|
note = conn.execute(
|
|
"SELECT * FROM notes WHERE id=? AND user_id=?", (note_id, user["id"])
|
|
).fetchone()
|
|
if not note:
|
|
raise HTTPException(404, "Notiz nicht gefunden.")
|
|
|
|
updates = {}
|
|
if data.text is not None:
|
|
if not data.text.strip():
|
|
raise HTTPException(400, "Notiz darf nicht leer sein.")
|
|
updates["text"] = data.text.strip()
|
|
if data.meta_json is not None:
|
|
updates["meta_json"] = json.dumps(data.meta_json)
|
|
if data.location_name is not None:
|
|
updates["location_name"] = data.location_name
|
|
if data.parent_label is not None:
|
|
updates["parent_label"] = data.parent_label
|
|
|
|
if not updates:
|
|
return _serialize(note)
|
|
|
|
updates["updated_at"] = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
|
set_clause = ", ".join(f"{k}=?" for k in updates)
|
|
values = list(updates.values()) + [note_id]
|
|
conn.execute(f"UPDATE notes SET {set_clause} WHERE id=?", values)
|
|
row = conn.execute("SELECT * FROM notes WHERE id=?", (note_id,)).fetchone()
|
|
return _serialize(row)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# DELETE /api/notes/{id}
|
|
# ------------------------------------------------------------------
|
|
@router.delete("/{note_id}", status_code=204)
|
|
async def delete_note(note_id: int, user=Depends(get_current_user)):
|
|
with db() as conn:
|
|
note = conn.execute(
|
|
"SELECT id FROM notes WHERE id=? AND user_id=?", (note_id, user["id"])
|
|
).fetchone()
|
|
if not note:
|
|
raise HTTPException(404, "Notiz nicht gefunden.")
|
|
conn.execute("DELETE FROM notes WHERE id=?", (note_id,))
|
|
return None
|