retried with custom-api widgets

This commit is contained in:
2026-02-01 20:56:47 +01:00
parent 317cf4b180
commit 143706d856
2 changed files with 677 additions and 4 deletions
+258 -1
View File
@@ -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