From 35c71a1e6d3af62391110e4c469b0c6152a30e09 Mon Sep 17 00:00:00 2001 From: schmeeve Date: Wed, 23 Sep 2026 13:01:22 -0700 Subject: [PATCH] homepage --- backend/main.py | 5 +++++ backend/notes.py | 30 ++++++++++++++++++++++++++++++ extras/homepage-widget.yaml | 27 +++++++++++++++++++++++++++ tests/test_main.py | 18 ++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 extras/homepage-widget.yaml diff --git a/backend/main.py b/backend/main.py index 813e122..1434278 100644 --- a/backend/main.py +++ b/backend/main.py @@ -233,5 +233,10 @@ async def search(q: str, _: None = Depends(require_access)): return await notes.search_notes(q) +@app.get("/api/homepage") +async def homepage_stats(_: None = Depends(require_access)): + return await notes.get_homepage_stats() + + # Mount static assets last so /api routes take precedence app.mount("/static", StaticFiles(directory="static"), name="static") diff --git a/backend/notes.py b/backend/notes.py index c90f737..9beb97f 100644 --- a/backend/notes.py +++ b/backend/notes.py @@ -346,6 +346,36 @@ async def _record_history( ) +async def get_homepage_stats() -> dict: + from pathlib import Path + + async with aiosqlite.connect(settings.database_path) as db: + db.row_factory = aiosqlite.Row + + async def count(query: str) -> int: + row = await db.execute(query) + row = await row.fetchone() + return row[0] + + total_notes = await count("SELECT COUNT(*) FROM notes WHERE deleted_at IS NULL") + actions = await count("SELECT COUNT(*) FROM history") + users = await count("SELECT COUNT(*) FROM users") + + attachments = 0 + data_root = Path(settings.data_dir) + if data_root.is_dir(): + for attach_dir in data_root.rglob("attachments"): + if attach_dir.is_dir(): + attachments += sum(1 for f in attach_dir.iterdir() if f.is_file()) + + return { + "total_notes": total_notes, + "attachments": attachments, + "actions": actions, + "users": users, + } + + async def get_history(note_id: str) -> list[dict]: async with aiosqlite.connect(settings.database_path) as db: db.row_factory = aiosqlite.Row diff --git a/extras/homepage-widget.yaml b/extras/homepage-widget.yaml new file mode 100644 index 0000000..d0cde9e --- /dev/null +++ b/extras/homepage-widget.yaml @@ -0,0 +1,27 @@ +# Paste this into /opt/homepage/config/services.yaml as a new entry. +# +# Replace {{ SNOTES_URL }} with where snotes is reachable, e.g. +# http://192.168.1.45:8019 (host mapped port) + +- Snotes: + icon: snotes-main.png + href: "{{ SNOTES_URL }}" + description: Markdown notes + widget: + type: customapi + url: "{{ SNOTES_URL }}/api/homepage" + refreshInterval: 60000 + display: block + mappings: + - field: total_notes + label: Notes + format: number + - field: attachments + label: Attachments + format: number + - field: actions + label: Actions + format: number + - field: users + label: Users + format: number diff --git a/tests/test_main.py b/tests/test_main.py index 7721b5a..0d3da8a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -153,6 +153,24 @@ async def test_history_recorded(client): assert history[0]["ip"] == "192.168.1.5" +async def test_homepage_stats(client): + await client.post("/api/notes", json={"body": "note one"}) + await client.post("/api/notes", json={"body": "note two"}) + await client.post( + "/api/attachments", + files={"file": ("a.png", b"abc", "image/png")}, + data={"username": "schmeeve"}, + ) + + r = await client.get("/api/homepage") + assert r.status_code == 200 + stats = r.json() + assert stats["total_notes"] == 2 + assert stats["attachments"] == 1 + assert stats["actions"] == 2 + assert stats["users"] == 3 + + async def test_user_selection_cookie(client): r = await client.post("/api/users/select", json={"username": "ilya"}) assert r.status_code == 200