diff --git a/glance-system/glance-helper.yaml b/glance-system/glance-helper.yaml
index 64e1585..7af2b64 100644
--- a/glance-system/glance-helper.yaml
+++ b/glance-system/glance-helper.yaml
@@ -28,725 +28,6 @@ data:
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", accent: str = "ffffff", bgcolor: str = ""):
- """Serve the notes widget HTML page for a specific user with optional theme."""
- 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"
- # Sanitize accent color (hex only, default to white)
- safe_accent = re.sub(r'[^a-fA-F0-9]', '', accent)[:6] or "ffffff"
- # Sanitize bgcolor (hex only, default to dark purple matching Orsi's theme)
- safe_bgcolor = re.sub(r'[^a-fA-F0-9]', '', bgcolor)[:6] or "2d1f3d"
-
- current_notes = load_notes(safe_user)
- escaped_notes = current_notes.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
-
- html = f"""
-
-
-
-
-
-
-
-
-
-
- """
- 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)
-
- # ================================
- # Unified User Data System (Notes, Todo, Motivation)
- # ================================
- # File format:
- # [Notes]
- # Free-form notes text...
- #
- # [Todo]
- # - [ ] Uncompleted task
- # - [x] Completed task
- #
- # [Motivation]
- # Quote 1
- # Quote 2
-
- def get_userdata_file(user: str) -> str:
- """Get user data file path, with validation."""
- safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user) or "default"
- data_dir = os.environ.get("DATA_DIR", "/data")
- return os.path.join(data_dir, f"userdata_{safe_user}.txt")
-
- def load_userdata(user: str) -> dict:
- """Load and parse user data file into sections."""
- filepath = get_userdata_file(user)
- sections = {"Notes": "", "Todo": [], "Motivation": []}
- try:
- if os.path.exists(filepath):
- with open(filepath, "r", encoding="utf-8") as f:
- content = f.read()
- current_section = None
- section_lines = []
- for line in content.split('\n'):
- stripped = line.strip()
- if stripped.startswith('[') and stripped.endswith(']'):
- # Save previous section
- if current_section == "Notes":
- sections["Notes"] = '\n'.join(section_lines).strip()
- elif current_section == "Todo":
- for l in section_lines:
- l = l.strip()
- if l.startswith('- [x] '):
- sections["Todo"].append({"text": l[6:], "done": True})
- elif l.startswith('- [ ] '):
- sections["Todo"].append({"text": l[6:], "done": False})
- elif current_section == "Motivation":
- for l in section_lines:
- l = l.strip()
- if l:
- sections["Motivation"].append(l)
- # Start new section
- current_section = stripped[1:-1]
- section_lines = []
- else:
- section_lines.append(line)
- # Handle last section
- if current_section == "Notes":
- sections["Notes"] = '\n'.join(section_lines).strip()
- elif current_section == "Todo":
- for l in section_lines:
- l = l.strip()
- if l.startswith('- [x] '):
- sections["Todo"].append({"text": l[6:], "done": True})
- elif l.startswith('- [ ] '):
- sections["Todo"].append({"text": l[6:], "done": False})
- elif current_section == "Motivation":
- for l in section_lines:
- l = l.strip()
- if l:
- sections["Motivation"].append(l)
- except Exception as e:
- print(f"Error loading userdata for {user}: {e}")
- return sections
-
- def save_userdata(user: str, sections: dict) -> bool:
- """Save sections back to user data file."""
- filepath = get_userdata_file(user)
- try:
- lines = []
- lines.append("[Notes]")
- lines.append(sections.get("Notes", ""))
- lines.append("")
- lines.append("[Todo]")
- for item in sections.get("Todo", []):
- mark = "x" if item.get("done") else " "
- lines.append(f"- [{mark}] {item.get('text', '')}")
- lines.append("")
- lines.append("[Motivation]")
- for quote in sections.get("Motivation", []):
- lines.append(quote)
- with open(filepath, "w", encoding="utf-8") as f:
- f.write('\n'.join(lines))
- return True
- except Exception as e:
- print(f"Error saving userdata for {user}: {e}")
- return False
-
- # --------------------------------
- # Todo Widget
- # --------------------------------
- @APP.get("/userdata/todo")
- def todo_widget(key: str = "", user: str = "default", accent: str = "5ac8d8", bgcolor: str = "transparent"):
- """Serve the Todo widget HTML page."""
- 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"
- safe_accent = re.sub(r'[^a-fA-F0-9]', '', accent)[:6] or "5ac8d8"
- # Handle transparent background
- if bgcolor == "transparent" or not bgcolor:
- bg_style = "transparent"
- else:
- safe_bgcolor = re.sub(r'[^a-fA-F0-9]', '', bgcolor)[:6]
- bg_style = f"#{safe_bgcolor}" if safe_bgcolor else "transparent"
-
- sections = load_userdata(safe_user)
- todos = sections.get("Todo", [])
-
- # Build todo items HTML
- todo_html = ""
- for i, item in enumerate(todos):
- checked = "checked" if item.get("done") else ""
- text_escaped = item.get("text", "").replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
- done_class = "done" if item.get("done") else ""
- todo_html += f'''
-
- {text_escaped}
-
-
'''
-
- html = f"""
-
-
-
-
-
-
-
-
-
-
-
-
-
{todo_html if todo_html else '
No tasks yet
'}
-
-
-
-
- """
- return Response(content=html, media_type="text/html")
-
- @APP.post("/userdata/todo/add")
- async def todo_add(key: str = "", user: str = "default", content: dict = None):
- 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.get("text"):
- sections = load_userdata(safe_user)
- sections["Todo"].append({"text": content["text"].strip(), "done": False})
- if save_userdata(safe_user, sections):
- return {"status": "ok"}
- return Response(content="Failed", status_code=500)
-
- @APP.post("/userdata/todo/toggle")
- async def todo_toggle(key: str = "", user: str = "default", index: int = 0):
- 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"
- sections = load_userdata(safe_user)
- if 0 <= index < len(sections["Todo"]):
- sections["Todo"][index]["done"] = not sections["Todo"][index]["done"]
- if save_userdata(safe_user, sections):
- return {"status": "ok"}
- return Response(content="Failed", status_code=500)
-
- @APP.post("/userdata/todo/delete")
- async def todo_delete(key: str = "", user: str = "default", index: int = 0):
- 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"
- sections = load_userdata(safe_user)
- if 0 <= index < len(sections["Todo"]):
- sections["Todo"].pop(index)
- if save_userdata(safe_user, sections):
- return {"status": "ok"}
- return Response(content="Failed", status_code=500)
-
- # --------------------------------
- # Motivation Widget
- # --------------------------------
- import random as _random
-
- @APP.get("/userdata/motivation")
- def motivation_widget(key: str = "", user: str = "default", accent: str = "5ac8d8", bgcolor: str = "transparent"):
- """Serve the Motivation widget HTML page with random quote."""
- 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"
- safe_accent = re.sub(r'[^a-fA-F0-9]', '', accent)[:6] or "5ac8d8"
- if bgcolor == "transparent" or not bgcolor:
- bg_style = "transparent"
- else:
- safe_bgcolor = re.sub(r'[^a-fA-F0-9]', '', bgcolor)[:6]
- bg_style = f"#{safe_bgcolor}" if safe_bgcolor else "transparent"
-
- sections = load_userdata(safe_user)
- quotes = sections.get("Motivation", [])
-
- # Pick random quote
- current_quote = _random.choice(quotes) if quotes else "Add your first motivational quote!"
- current_quote_escaped = current_quote.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
-
- # Build manage URL (opens in new tab)
- manage_url = f"/userdata/motivation/manage?key={expected_key}&user={safe_user}&accent={safe_accent}"
-
- html = f"""
-
-
-
-
-
-
-
-
-
⚙️
-
-
"{current_quote_escaped}"
-
-
-
-
- """
- return Response(content=html, media_type="text/html")
-
- @APP.get("/userdata/motivation/manage")
- def motivation_manage(key: str = "", user: str = "default", accent: str = "5ac8d8"):
- """Full-page management interface for motivation quotes."""
- 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"
- safe_accent = re.sub(r'[^a-fA-F0-9]', '', accent)[:6] or "5ac8d8"
-
- sections = load_userdata(safe_user)
- quotes = sections.get("Motivation", [])
-
- # Build quotes list HTML
- quotes_html = ""
- for i, q in enumerate(quotes):
- q_escaped = q.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
- quotes_html += f'''
- {i+1}.
- {q_escaped}
-
-
'''
-
- html = f"""
-
-
-
-
- Manage Motivation Quotes
-
-
-
-
-
-
-
-
-
-
-
- {quotes_html if quotes_html else '
No quotes yet. Add your first motivational quote above!
'}
-
-
Close this tab when done. Refresh your dashboard to see changes.
-
-
-
- """
- return Response(content=html, media_type="text/html")
-
- @APP.post("/userdata/motivation/add")
- async def motivation_add(key: str = "", user: str = "default", content: dict = None):
- 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.get("text"):
- sections = load_userdata(safe_user)
- sections["Motivation"].append(content["text"].strip())
- if save_userdata(safe_user, sections):
- return {"status": "ok"}
- return Response(content="Failed", status_code=500)
-
- @APP.post("/userdata/motivation/delete")
- async def motivation_delete(key: str = "", user: str = "default", index: int = 0):
- 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"
- sections = load_userdata(safe_user)
- if 0 <= index < len(sections["Motivation"]):
- sections["Motivation"].pop(index)
- if save_userdata(safe_user, sections):
- return {"status": "ok"}
- return Response(content="Failed", status_code=500)
-
- @APP.get("/userdata/motivation/random")
- def motivation_random(key: str = "", user: str = "default"):
- """Get a random motivation quote as JSON."""
- 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"
- sections = load_userdata(safe_user)
- quotes = sections.get("Motivation", [])
- if quotes:
- return {"quote": _random.choice(quotes), "total": len(quotes)}
- return {"quote": "", "total": 0}
-
# ================================
# Időkép configuration
# ================================
diff --git a/glance-system/glance-kisfenyo.yaml b/glance-system/glance-kisfenyo.yaml
index d7b498b..26ef292 100644
--- a/glance-system/glance-kisfenyo.yaml
+++ b/glance-system/glance-kisfenyo.yaml
@@ -764,11 +764,7 @@ data:
- type: calendar
first-day-of-week: monday
# Tasks (persistent, file-based)
- - type: iframe
- css-class: iframe-no-tint
- source: https://glance-helper.dooplex.hu/userdata/todo?key=oplQqnLnJK2vErRVYJpvVUcSDBOSbCHZSbsYY2bwSifgTMfT&user=kisfenyo&accent=5ac8d8&bgcolor=0f1c24
- height: 200
- title: Tasks
+ - type: to-do
# Outline Notes iframe
- type: iframe
source: https://outline.dooplex.hu/collection/dooplex-server-iTAZn04AaR/recent
@@ -852,12 +848,6 @@ data:
{{ end }}
- # Motivation Quote
- - type: iframe
- css-class: iframe-no-tint
- source: https://glance-helper.dooplex.hu/userdata/motivation?key=oplQqnLnJK2vErRVYJpvVUcSDBOSbCHZSbsYY2bwSifgTMfT&user=kisfenyo&accent=5ac8d8&bgcolor=0f1c24
- height: 80
- title: Motivation
# Calendar Events Widget (Családi only)
- type: custom-api