From 345d7576fee9ee12c5032dec7e5d0ac999e29880 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Thu, 11 Jun 2026 00:20:47 -0700 Subject: [PATCH] feat: app list page on auth.paperclover.net/apps closes #16 generate-app-list.py renders config/static/apps.html from the net.paperclover.list.* compose labels, grouped by access level; caddy serves it at auth./apps. regenerate with: sh docker.sh compose config --format json | python3 generate-app-list.py > config/static/apps.html also fixes two wrong domain labels (sonarr jkt->snr, navidrome nd->music) so the generated links match the Caddyfile. --- compose.yaml | 4 +- config/Caddyfile | 6 +++ config/static/apps.html | 83 +++++++++++++++++++++++++++++++ generate-app-list.py | 106 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 config/static/apps.html create mode 100644 generate-app-list.py diff --git a/compose.yaml b/compose.yaml index ea9f61ead4255d47011ad00319d191ef58b9e9cb..f7e5e8758fbfbc9f0e11890d8b498d0dbc1e008f 100755 --- a/compose.yaml +++ b/compose.yaml @@ -336,7 +336,7 @@ services: ND_PORT: "80" labels: net.paperclover.list.name: Navidrome Music - net.paperclover.list.domain: nd + net.paperclover.list.domain: music net.paperclover.list.priority: 109 net.paperclover.list.access: media depends_on: @@ -419,7 +419,7 @@ services: restart: unless-stopped labels: net.paperclover.list.name: Sonarr TV PVR - net.paperclover.list.domain: jkt + net.paperclover.list.domain: snr net.paperclover.list.priority: 51 net.paperclover.list.access: media-manage radarr: # port 7878 diff --git a/config/Caddyfile b/config/Caddyfile index 662ac5110146defbdd1ec4d1da5c5a337bfc1f37..1604bdc69ea3bd61f7566de83fd0ea66cb2e6d9e 100644 --- a/config/Caddyfile +++ b/config/Caddyfile @@ -44,6 +44,12 @@ auth.{$HOME_DOMAIN} { handle / { redir / /realms/master/account + } + handle /apps { + rewrite * /static/apps.html + file_server { + root /etc/caddy + } } reverse_proxy "http://keycloak" } diff --git a/config/static/apps.html b/config/static/apps.html new file mode 100644 index 0000000000000000000000000000000000000000..8990e80d3b86b22c3065e1bab900b1420871976c --- /dev/null +++ b/config/static/apps.html @@ -0,0 +1,83 @@ + + + + + + +apps (snow sign on) + + + +

❄︎ apps on paperclover.net

+
+
+

for everyone

+

any snow sign on account

+ +
+
+

media

+

members of the media group

+ +
+
+

media management

+

members of the media-manage group

+ +
+
+

personal

+

just clover

+ +
+
+
+

signed in with snow sign on — + one account for everything here.

+

missing access? contact clover.

+
+ + diff --git a/generate-app-list.py b/generate-app-list.py new file mode 100644 index 0000000000000000000000000000000000000000..2275c51c5e9694d5833e6db98435f61ae96a634a --- /dev/null +++ b/generate-app-list.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# generates config/static/apps.html from the net.paperclover.list.* labels +# in compose.yaml. usage: +# sh docker.sh compose config --format json | python3 generate-app-list.py > config/static/apps.html +# (or local: docker compose config --format json | ...) +import json +import os +import sys + +PFX = "net.paperclover.list." +DOMAIN = os.environ.get("HOME_DOMAIN", "paperclover.net") + +ACCESS_GROUPS = [ + (None, "for everyone", "any snow sign on account"), + ("media", "media", "members of the media group"), + ("media-manage", "media management", "members of the media-manage group"), + ("personal", "personal", "just clover"), +] + +cfg = json.load(sys.stdin) +apps = [] +for name, svc in cfg["services"].items(): + labels = svc.get("labels") or {} + if isinstance(labels, list): + labels = dict(l.split("=", 1) for l in labels) + if PFX + "name" not in labels: + continue + apps.append({ + "name": labels[PFX + "name"], + "domain": labels.get(PFX + "domain"), + "priority": int(labels.get(PFX + "priority", 0)), + "access": labels.get(PFX + "access"), + }) + +apps.sort(key=lambda a: -a["priority"]) + +def entries(access): + out = [] + for a in apps: + if a["access"] != access: + continue + if a["domain"]: + out.append( + f'
  • ' + f'{a["name"].lower()}' + f' {a["domain"]}.{DOMAIN}
  • ' + ) + else: + out.append(f'
  • {a["name"].lower()} (no web ui)
  • ') + return out + +sections = [] +for access, title, who in ACCESS_GROUPS: + items = entries(access) + if not items: + continue + sections.append( + f'
    \n

    {title}

    \n' + f'

    {who}

    \n
      \n' + + "\n".join(" " + i for i in items) + + "\n
    \n
    " + ) + +body = "\n".join(sections) + +print(f""" + + + + + +apps (snow sign on) + + + +

    ❄︎ apps on {DOMAIN}

    +
    +{body} +
    + + +""") -- 2.54.0