added notes feature

This commit is contained in:
2026-01-23 09:14:56 +01:00
parent 121c1056e4
commit 2dfe56ee53
2 changed files with 172 additions and 1 deletions
+168 -1
View File
@@ -27,7 +27,174 @@ data:
from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST
APP = FastAPI() APP = FastAPI()
# ================================
# Simple Notes Widget - Multi-user
# ================================
def get_notes_file(user: str) -> str:
"""Get notes file path for a user, with validation."""
# Sanitize username: only allow alphanumeric, dash, underscore
safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user)
if not safe_user:
safe_user = "default"
data_dir = os.environ.get("DATA_DIR", "/data")
return os.path.join(data_dir, f"notes_{safe_user}.txt")
def load_notes(user: str) -> str:
"""Load notes from file for a specific user."""
notes_file = get_notes_file(user)
try:
if os.path.exists(notes_file):
with open(notes_file, "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"Error loading notes for {user}: {e}")
return ""
def save_notes(user: str, content: str) -> bool:
"""Save notes to file for a specific user."""
notes_file = get_notes_file(user)
try:
with open(notes_file, "w", encoding="utf-8") as f:
f.write(content)
return True
except Exception as e:
print(f"Error saving notes for {user}: {e}")
return False
@APP.get("/notes")
def notes_widget(key: str = "", user: str = "default"):
"""Serve the notes widget HTML page for a specific user."""
expected_key = os.environ.get("GLANCE_HELPER_KEY", "")
if key != expected_key:
return Response(content="Unauthorized", status_code=401)
# Sanitize user for display
safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user) or "default"
current_notes = load_notes(safe_user)
# Escape for safe HTML embedding
escaped_notes = current_notes.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
html = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
background: transparent;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
}}
.container {{
display: flex;
flex-direction: column;
height: 100%;
padding: 8px;
}}
.status {{
font-size: 11px;
color: rgba(255, 255, 255, 0.5);
padding: 4px 0;
text-align: right;
min-height: 20px;
}}
.status.saving {{ color: rgba(255, 200, 100, 0.8); }}
.status.saved {{ color: rgba(100, 255, 150, 0.8); }}
.status.error {{ color: rgba(255, 100, 100, 0.8); }}
textarea {{
flex: 1;
width: 100%;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
line-height: 1.5;
padding: 12px;
resize: none;
outline: none;
}}
textarea:focus {{
border-color: rgba(180, 130, 220, 0.5);
background: rgba(255, 255, 255, 0.08);
}}
textarea::placeholder {{
color: rgba(255, 255, 255, 0.3);
}}
</style>
</head>
<body>
<div class="container">
<textarea id="notes" placeholder="Write your notes here...">{escaped_notes}</textarea>
<div class="status" id="status"></div>
</div>
<script>
const textarea = document.getElementById('notes');
const status = document.getElementById('status');
const apiKey = '{expected_key}';
const user = '{safe_user}';
let saveTimeout = null;
let lastSaved = textarea.value;
function updateStatus(text, className) {{
status.textContent = text;
status.className = 'status ' + (className || '');
}}
async function saveNotes() {{
const content = textarea.value;
if (content === lastSaved) return;
updateStatus('Saving...', 'saving');
try {{
const response = await fetch('/notes/save?key=' + apiKey + '&user=' + user, {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{ content: content }})
}});
if (response.ok) {{
lastSaved = content;
updateStatus('Saved', 'saved');
setTimeout(() => updateStatus(''), 2000);
}} else {{
updateStatus('Save failed', 'error');
}}
}} catch (e) {{
updateStatus('Save failed', 'error');
}}
}}
textarea.addEventListener('input', () => {{
if (saveTimeout) clearTimeout(saveTimeout);
saveTimeout = setTimeout(saveNotes, 1000);
}});
textarea.addEventListener('blur', saveNotes);
window.addEventListener('beforeunload', saveNotes);
</script>
</body>
</html>"""
return Response(content=html, media_type="text/html")
@APP.post("/notes/save")
async def save_notes_api(key: str = "", user: str = "default", content: dict = None):
"""API endpoint to save notes for a specific user."""
expected_key = os.environ.get("GLANCE_HELPER_KEY", "")
if key != expected_key:
return Response(content="Unauthorized", status_code=401)
safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user) or "default"
if content and "content" in content:
if save_notes(safe_user, content["content"]):
return {"status": "ok", "user": safe_user}
return Response(content="Failed to save", status_code=500)
# ================================ # ================================
# Időkép configuration # Időkép configuration
# ================================ # ================================
+4
View File
@@ -591,6 +591,10 @@ data:
columns: columns:
- size: small - size: small
widgets: widgets:
- type: iframe
source: https://glance-helper.dooplex.hu/notes?key=oplQqnLnJK2vErRVYJpvVUcSDBOSbCHZSbsYY2bwSifgTMfT&user=orsi
height: 250
title: Quick Notes
- type: bookmarks - type: bookmarks
title: Links for Teaching title: Links for Teaching
groups: groups: