diff --git a/glance-system/glance-helper.yaml b/glance-system/glance-helper.yaml index 7af2b64..21c7802 100644 --- a/glance-system/glance-helper.yaml +++ b/glance-system/glance-helper.yaml @@ -990,6 +990,263 @@ data: }, ensure_ascii=False), media_type="application/json; charset=utf-8" ) + + # ================================ + # User Data Management (Notes, Todos, Motivation) + # ================================ + # File structure per user: /data/userdata-{username}.json + # { + # "notes": "free text...", + # "todos": [{"id": "uuid", "text": "...", "done": false, "created": "..."}], + # "motivation": ["quote1", "quote2", ...] + # } + + import uuid as uuid_lib + from pydantic import BaseModel + from typing import Optional, List + + class TodoItem(BaseModel): + text: str + done: bool = False + + class MotivationItem(BaseModel): + text: str + + class NotesUpdate(BaseModel): + content: str + + def _userdata_path(user: str) -> Path: + """Get the path to user data file.""" + # Sanitize username to prevent path traversal + safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user) + if not safe_user: + raise HTTPException(status_code=400, detail="Invalid username") + return DATA_DIR / f"userdata-{safe_user}.json" + + def _load_userdata(user: str) -> dict: + """Load user data or return default structure.""" + default = { + "notes": "", + "todos": [], + "motivation": [ + "Believe in yourself!", + "Every day is a new opportunity.", + "You've got this!", + "Small steps lead to big changes.", + "Stay focused, stay positive." + ] + } + path = _userdata_path(user) + data = _load_json(path, default) + # Ensure all keys exist + for key in default: + if key not in data: + data[key] = default[key] + return data + + def _save_userdata(user: str, data: dict) -> None: + """Save user data to file.""" + path = _userdata_path(user) + _save_json(path, data) + + def _verify_key(key: str = Query(default="")): + """Verify API key for write operations.""" + if not GLANCE_HELPER_KEY: + return True # No key configured, allow all + if key != GLANCE_HELPER_KEY: + raise HTTPException(status_code=403, detail="Invalid API key") + return True + + # ========== GET ALL USER DATA ========== + @APP.get("/userdata/{user}") + def get_userdata(user: str): + """Get all user data (notes, todos, motivation).""" + data = _load_userdata(user) + return Response( + content=json.dumps(data, ensure_ascii=False), + media_type="application/json; charset=utf-8" + ) + + # ========== NOTES ========== + @APP.get("/userdata/{user}/notes") + def get_notes(user: str): + """Get user notes.""" + data = _load_userdata(user) + return Response( + content=json.dumps({"notes": data.get("notes", "")}, ensure_ascii=False), + media_type="application/json; charset=utf-8" + ) + + @APP.post("/userdata/{user}/notes") + def update_notes(user: str, body: NotesUpdate, key: str = Query(default="")): + """Update user notes.""" + _verify_key(key) + data = _load_userdata(user) + data["notes"] = body.content + _save_userdata(user, data) + return Response( + content=json.dumps({"success": True, "notes": data["notes"]}, ensure_ascii=False), + media_type="application/json; charset=utf-8" + ) + + # ========== TODOS ========== + @APP.get("/userdata/{user}/todos") + def get_todos(user: str): + """Get user todos.""" + data = _load_userdata(user) + return Response( + content=json.dumps({ + "todos": data.get("todos", []), + "count": len(data.get("todos", [])) + }, ensure_ascii=False), + media_type="application/json; charset=utf-8" + ) + + @APP.post("/userdata/{user}/todos") + def add_todo(user: str, body: TodoItem, key: str = Query(default="")): + """Add a new todo item.""" + _verify_key(key) + data = _load_userdata(user) + + new_todo = { + "id": str(uuid_lib.uuid4())[:8], + "text": body.text.strip(), + "done": body.done, + "created": datetime.now(timezone.utc).isoformat() + } + + if not data.get("todos"): + data["todos"] = [] + data["todos"].append(new_todo) + _save_userdata(user, data) + + return Response( + content=json.dumps({"success": True, "todo": new_todo}, ensure_ascii=False), + media_type="application/json; charset=utf-8" + ) + + @APP.put("/userdata/{user}/todos/{todo_id}") + def update_todo(user: str, todo_id: str, body: TodoItem, key: str = Query(default="")): + """Update a todo item (toggle done, update text).""" + _verify_key(key) + data = _load_userdata(user) + + for todo in data.get("todos", []): + if todo.get("id") == todo_id: + todo["text"] = body.text.strip() + todo["done"] = body.done + _save_userdata(user, data) + return Response( + content=json.dumps({"success": True, "todo": todo}, ensure_ascii=False), + media_type="application/json; charset=utf-8" + ) + + raise HTTPException(status_code=404, detail="Todo not found") + + @APP.delete("/userdata/{user}/todos/{todo_id}") + def delete_todo(user: str, todo_id: str, key: str = Query(default="")): + """Delete a todo item.""" + _verify_key(key) + data = _load_userdata(user) + + original_count = len(data.get("todos", [])) + data["todos"] = [t for t in data.get("todos", []) if t.get("id") != todo_id] + + if len(data["todos"]) == original_count: + raise HTTPException(status_code=404, detail="Todo not found") + + _save_userdata(user, data) + return Response( + content=json.dumps({"success": True, "deleted": todo_id}, ensure_ascii=False), + media_type="application/json; charset=utf-8" + ) + + # ========== MOTIVATION ========== + @APP.get("/userdata/{user}/motivation") + def get_motivation(user: str): + """Get all motivation quotes.""" + data = _load_userdata(user) + quotes = data.get("motivation", []) + return Response( + content=json.dumps({ + "quotes": quotes, + "count": len(quotes) + }, ensure_ascii=False), + media_type="application/json; charset=utf-8" + ) + + @APP.get("/userdata/{user}/motivation/random") + def get_random_motivation(user: str): + """Get a random motivation quote.""" + data = _load_userdata(user) + quotes = data.get("motivation", []) + + if not quotes: + return Response( + content=json.dumps({ + "quote": "Add some motivation quotes!", + "index": -1, + "total": 0 + }, ensure_ascii=False), + media_type="application/json; charset=utf-8" + ) + + idx = random.randint(0, len(quotes) - 1) + return Response( + content=json.dumps({ + "quote": quotes[idx], + "index": idx, + "total": len(quotes) + }, ensure_ascii=False), + media_type="application/json; charset=utf-8" + ) + + @APP.post("/userdata/{user}/motivation") + def add_motivation(user: str, body: MotivationItem, key: str = Query(default="")): + """Add a new motivation quote.""" + _verify_key(key) + data = _load_userdata(user) + + if not data.get("motivation"): + data["motivation"] = [] + + quote_text = body.text.strip() + if quote_text and quote_text not in data["motivation"]: + data["motivation"].append(quote_text) + _save_userdata(user, data) + + return Response( + content=json.dumps({ + "success": True, + "quotes": data["motivation"], + "count": len(data["motivation"]) + }, ensure_ascii=False), + media_type="application/json; charset=utf-8" + ) + + @APP.delete("/userdata/{user}/motivation/{index}") + def delete_motivation(user: str, index: int, key: str = Query(default="")): + """Delete a motivation quote by index.""" + _verify_key(key) + data = _load_userdata(user) + quotes = data.get("motivation", []) + + if index < 0 or index >= len(quotes): + raise HTTPException(status_code=404, detail="Quote not found") + + deleted = quotes.pop(index) + _save_userdata(user, data) + + return Response( + content=json.dumps({ + "success": True, + "deleted": deleted, + "quotes": quotes, + "count": len(quotes) + }, ensure_ascii=False), + media_type="application/json; charset=utf-8" + ) + --- apiVersion: apps/v1 kind: Deployment @@ -1022,7 +1279,7 @@ spec: apt-get update; apt-get install -y --no-install-recommends curl ca-certificates iputils-ping dnsutils tzdata net-tools; rm -rf /var/lib/apt/lists/*; - pip install --no-cache-dir fastapi uvicorn requests beautifulsoup4 prometheus-client icalendar python-dateutil; + pip install --no-cache-dir fastapi uvicorn requests beautifulsoup4 prometheus-client icalendar python-dateutil pydantic; python -c "import uvicorn; uvicorn.run('app:APP', host='0.0.0.0', port=8000)" command: - /bin/sh diff --git a/glance-system/glance-kisfenyo.yaml b/glance-system/glance-kisfenyo.yaml index 26ef292..2d4e9aa 100644 --- a/glance-system/glance-kisfenyo.yaml +++ b/glance-system/glance-kisfenyo.yaml @@ -763,8 +763,171 @@ data: # Calendar Widget - type: calendar first-day-of-week: monday - # Tasks (persistent, file-based) - - type: to-do + # To-Do List Widget (custom-api with persistent storage) + - type: custom-api + title: To-Do + cache: 30s + url: http://glance-helper.glance-system.svc.cluster.local:8000/userdata/kisfenyo/todos + options: + api_base: https://glance-helper.dooplex.hu + user: kisfenyo + api_key: oplQqnLnJK2vErRVYJpvVUcSDBOSbCHZSbsYY2bwSifgTMfT + template: | + {{ $apiBase := .Options.StringOr "api_base" "https://glance-helper.dooplex.hu" }} + {{ $user := .Options.StringOr "user" "kisfenyo" }} + {{ $apiKey := .Options.StringOr "api_key" "" }} + {{ $todos := .JSON.Array "todos" }} + {{ $widgetId := "todo-kisfenyo" }} + + + +
+ + # Outline Notes iframe - type: iframe source: https://outline.dooplex.hu/collection/dooplex-server-iTAZn04AaR/recent @@ -848,6 +1011,259 @@ data: {{ end }} + # Motivation Quote Widget + - type: custom-api + title: Motivation + cache: 1m + url: http://glance-helper.glance-system.svc.cluster.local:8000/userdata/kisfenyo/motivation/random + options: + api_base: https://glance-helper.dooplex.hu + user: kisfenyo + api_key: oplQqnLnJK2vErRVYJpvVUcSDBOSbCHZSbsYY2bwSifgTMfT + template: | + {{ $apiBase := .Options.StringOr "api_base" "https://glance-helper.dooplex.hu" }} + {{ $user := .Options.StringOr "user" "kisfenyo" }} + {{ $apiKey := .Options.StringOr "api_key" "" }} + {{ $quote := .JSON.String "quote" }} + {{ $total := .JSON.Int "total" }} + {{ $widgetId := "motiv-kisfenyo" }} + + + + + + + + + + # Calendar Events Widget (Családi only) - type: custom-api @@ -2341,4 +2757,4 @@ spec: - hosts: - kisfenyo.dooplex.hu secretName: glance-kisfenyo-tls ---- +--- \ No newline at end of file