| author | |
| committer | |
| log | 345d7576fee9ee12c5032dec7e5d0ac999e29880 |
| tree | 50ee779247e95f298c8486096999645e1c922fcc |
| parent | 0afc7d74ea327a98280729334118af95fbc07a0c |
| signature |
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.<domain>/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.4 files changed, 197 insertions(+), 2 deletions(-)
compose.yaml+2-2| ... | @@ -336,7 +336,7 @@ services: | ... | @@ -336,7 +336,7 @@ services: |
| 336 | ND_PORT: "80" | 336 | ND_PORT: "80" |
| 337 | labels: | 337 | labels: |
| 338 | net.paperclover.list.name: Navidrome Music | 338 | net.paperclover.list.name: Navidrome Music |
| 339 | net.paperclover.list.domain: nd | 339 | net.paperclover.list.domain: music |
| 340 | net.paperclover.list.priority: 109 | 340 | net.paperclover.list.priority: 109 |
| 341 | net.paperclover.list.access: media | 341 | net.paperclover.list.access: media |
| 342 | depends_on: | 342 | depends_on: |
| ... | @@ -419,7 +419,7 @@ services: | ... | @@ -419,7 +419,7 @@ services: |
| 419 | restart: unless-stopped | 419 | restart: unless-stopped |
| 420 | labels: | 420 | labels: |
| 421 | net.paperclover.list.name: Sonarr TV PVR | 421 | net.paperclover.list.name: Sonarr TV PVR |
| 422 | net.paperclover.list.domain: jkt | 422 | net.paperclover.list.domain: snr |
| 423 | net.paperclover.list.priority: 51 | 423 | net.paperclover.list.priority: 51 |
| 424 | net.paperclover.list.access: media-manage | 424 | net.paperclover.list.access: media-manage |
| 425 | radarr: # port 7878 | 425 | radarr: # port 7878 |
config/Caddyfile+6| ... | @@ -44,6 +44,12 @@ | ... | @@ -44,6 +44,12 @@ |
| 44 | auth.{$HOME_DOMAIN} { | 44 | auth.{$HOME_DOMAIN} { |
| 45 | handle / { | 45 | handle / { |
| 46 | redir / /realms/master/account | 46 | redir / /realms/master/account |
| 47 | } | ||
| 48 | handle /apps { | ||
| 49 | rewrite * /static/apps.html | ||
| 50 | file_server { | ||
| 51 | root /etc/caddy | ||
| 52 | } | ||
| 47 | } | 53 | } |
| 48 | 	reverse_proxy "http://keycloak" | 54 | 	reverse_proxy "http://keycloak" |
| 49 | } | 55 | } |
config/static/apps.html created+83| ... | @@ -0,0 +1,83 @@ | ||
| 1 | <!doctype html> | ||
| 2 | <!-- generated by generate-app-list.py — do not edit by hand --> | ||
| 3 | <html lang="en"> | ||
| 4 | <head> | ||
| 5 | <meta charset="utf-8"> | ||
| 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| 7 | <title>apps (snow sign on)</title> | ||
| 8 | <style> | ||
| 9 | @import "https://file.paperclover.net/.static/font.css"; | ||
| 10 | :root { color-scheme: light dark; } | ||
| 11 | body { | ||
| 12 | font-family: "national-medium", system-ui, sans-serif; | ||
| 13 | background-color: light-dark(#e8eefa, #2f4b67); | ||
| 14 | color: light-dark(black, white); | ||
| 15 | max-width: 34rem; | ||
| 16 | margin: 0 auto; | ||
| 17 | padding: 2rem 1rem 4rem; | ||
| 18 | } | ||
| 19 | h1, h2, a { color: light-dark(#1a46cd, #938cff); } | ||
| 20 | h1 { text-align: center; font-weight: normal; } | ||
| 21 | a { text-decoration: dotted underline; } | ||
| 22 | a:hover { text-decoration: underline; } | ||
| 23 | .who { opacity: 0.6; font-size: 0.9rem; margin-top: -0.5rem; } | ||
| 24 | .domain { opacity: 0.5; font-size: 0.85rem; } | ||
| 25 | ul { list-style: "\2744\FE0E "; padding-left: 1.5rem; } | ||
| 26 | li { margin: 0.4rem 0; } | ||
| 27 | footer { margin-top: 3rem; opacity: 0.7; font-size: 0.9rem; text-align: center; } | ||
| 28 | </style> | ||
| 29 | </head> | ||
| 30 | <body> | ||
| 31 | <h1>&#x2744;&#xFE0E; apps on paperclover.net</h1> | ||
| 32 | <main> | ||
| 33 | <section> | ||
| 34 | <h2>for everyone</h2> | ||
| 35 | <p class="who">any snow sign on account</p> | ||
| 36 | <ul> | ||
| 37 | <li><a href="https://xmpp.paperclover.net">paper clover xmpp</a> <span class="domain">xmpp.paperclover.net</span></li> | ||
| 38 | <li><a href="https://spindle.paperclover.net">tangled spindle</a> <span class="domain">spindle.paperclover.net</span></li> | ||
| 39 | <li><a href="https://knot.paperclover.net">tangled knot</a> <span class="domain">knot.paperclover.net</span></li> | ||
| 40 | <li><a href="https://git.paperclover.net">clover git forge</a> <span class="domain">git.paperclover.net</span></li> | ||
| 41 | <li><a href="https://file.paperclover.net">copyparty file storage</a> <span class="domain">file.paperclover.net</span></li> | ||
| 42 | <li><a href="https://speedtest.paperclover.net">open speed test</a> <span class="domain">speedtest.paperclover.net</span></li> | ||
| 43 | </ul> | ||
| 44 | </section> | ||
| 45 | <section> | ||
| 46 | <h2>media</h2> | ||
| 47 | <p class="who">members of the media group</p> | ||
| 48 | <ul> | ||
| 49 | <li><a href="https://jelly.paperclover.net">cloverfin</a> <span class="domain">jelly.paperclover.net</span></li> | ||
| 50 | <li><a href="https://music.paperclover.net">navidrome music</a> <span class="domain">music.paperclover.net</span></li> | ||
| 51 | </ul> | ||
| 52 | </section> | ||
| 53 | <section> | ||
| 54 | <h2>media management</h2> | ||
| 55 | <p class="who">members of the media-manage group</p> | ||
| 56 | <ul> | ||
| 57 | <li><a href="https://qbt.paperclover.net">qbittorrent</a> <span class="domain">qbt.paperclover.net</span></li> | ||
| 58 | <li><a href="https://snr.paperclover.net">sonarr tv pvr</a> <span class="domain">snr.paperclover.net</span></li> | ||
| 59 | <li><a href="https://rdr.paperclover.net">radarr movie organizer</a> <span class="domain">rdr.paperclover.net</span></li> | ||
| 60 | <li><a href="https://jkt.paperclover.net">jackett api adapter</a> <span class="domain">jkt.paperclover.net</span></li> | ||
| 61 | </ul> | ||
| 62 | </section> | ||
| 63 | <section> | ||
| 64 | <h2>personal</h2> | ||
| 65 | <p class="who">just clover</p> | ||
| 66 | <ul> | ||
| 67 | <li><a href="https://opencode.paperclover.net">opencode</a> <span class="domain">opencode.paperclover.net</span></li> | ||
| 68 | <li><a href="https://sync.paperclover.net">syncthing</a> <span class="domain">sync.paperclover.net</span></li> | ||
| 69 | <li><a href="https://dawarich.paperclover.net">dawarich location history</a> <span class="domain">dawarich.paperclover.net</span></li> | ||
| 70 | <li><a href="https://pg.paperclover.net">pgadmin</a> <span class="domain">pg.paperclover.net</span></li> | ||
| 71 | <li><a href="https://redis.paperclover.net">redis insight</a> <span class="domain">redis.paperclover.net</span></li> | ||
| 72 | <li><a href="https://dns.paperclover.net">dns</a> <span class="domain">dns.paperclover.net</span></li> | ||
| 73 | <li>ddns <span class="domain">(no web ui)</span></li> | ||
| 74 | </ul> | ||
| 75 | </section> | ||
| 76 | </main> | ||
| 77 | <footer> | ||
| 78 | <p>signed in with <a href="https://auth.paperclover.net/realms/master/account">snow sign on</a> — | ||
| 79 | one account for everything here.</p> | ||
| 80 | <p>missing access? contact clover.</p> | ||
| 81 | </footer> | ||
| 82 | </body> | ||
| 83 | </html> | ||
generate-app-list.py created+106| ... | @@ -0,0 +1,106 @@ | ||
| 1 | #!/usr/bin/env python3 | ||
| 2 | # generates config/static/apps.html from the net.paperclover.list.* labels | ||
| 3 | # in compose.yaml. usage: | ||
| 4 | # sh docker.sh compose config --format json | python3 generate-app-list.py > config/static/apps.html | ||
| 5 | # (or local: docker compose config --format json | ...) | ||
| 6 | import json | ||
| 7 | import os | ||
| 8 | import sys | ||
| 9 | |||
| 10 | PFX = "net.paperclover.list." | ||
| 11 | DOMAIN = os.environ.get("HOME_DOMAIN", "paperclover.net") | ||
| 12 | |||
| 13 | ACCESS_GROUPS = [ | ||
| 14 | (None, "for everyone", "any snow sign on account"), | ||
| 15 | ("media", "media", "members of the media group"), | ||
| 16 | ("media-manage", "media management", "members of the media-manage group"), | ||
| 17 | ("personal", "personal", "just clover"), | ||
| 18 | ] | ||
| 19 | |||
| 20 | cfg = json.load(sys.stdin) | ||
| 21 | apps = [] | ||
| 22 | for name, svc in cfg["services"].items(): | ||
| 23 | labels = svc.get("labels") or {} | ||
| 24 | if isinstance(labels, list): | ||
| 25 | labels = dict(l.split("=", 1) for l in labels) | ||
| 26 | if PFX + "name" not in labels: | ||
| 27 | continue | ||
| 28 | apps.append({ | ||
| 29 | "name": labels[PFX + "name"], | ||
| 30 | "domain": labels.get(PFX + "domain"), | ||
| 31 | "priority": int(labels.get(PFX + "priority", 0)), | ||
| 32 | "access": labels.get(PFX + "access"), | ||
| 33 | }) | ||
| 34 | |||
| 35 | apps.sort(key=lambda a: -a["priority"]) | ||
| 36 | |||
| 37 | def entries(access): | ||
| 38 | out = [] | ||
| 39 | for a in apps: | ||
| 40 | if a["access"] != access: | ||
| 41 | continue | ||
| 42 | if a["domain"]: | ||
| 43 | out.append( | ||
| 44 | f' <li><a href="https://{a["domain"]}.{DOMAIN}">' | ||
| 45 | f'{a["name"].lower()}</a>' | ||
| 46 | f' <span class="domain">{a["domain"]}.{DOMAIN}</span></li>' | ||
| 47 | ) | ||
| 48 | else: | ||
| 49 | out.append(f' <li>{a["name"].lower()} <span class="domain">(no web ui)</span></li>') | ||
| 50 | return out | ||
| 51 | |||
| 52 | sections = [] | ||
| 53 | for access, title, who in ACCESS_GROUPS: | ||
| 54 | items = entries(access) | ||
| 55 | if not items: | ||
| 56 | continue | ||
| 57 | sections.append( | ||
| 58 | f' <section>\n <h2>{title}</h2>\n' | ||
| 59 | f' <p class="who">{who}</p>\n <ul>\n' | ||
| 60 | + "\n".join(" " + i for i in items) | ||
| 61 | + "\n </ul>\n </section>" | ||
| 62 | ) | ||
| 63 | |||
| 64 | body = "\n".join(sections) | ||
| 65 | |||
| 66 | print(f"""<!doctype html> | ||
| 67 | <!-- generated by generate-app-list.py — do not edit by hand --> | ||
| 68 | <html lang="en"> | ||
| 69 | <head> | ||
| 70 | <meta charset="utf-8"> | ||
| 71 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| 72 | <title>apps (snow sign on)</title> | ||
| 73 | <style> | ||
| 74 | @import "https://file.{DOMAIN}/.static/font.css"; | ||
| 75 | :root {{ color-scheme: light dark; }} | ||
| 76 | body {{ | ||
| 77 | font-family: "national-medium", system-ui, sans-serif; | ||
| 78 | background-color: light-dark(#e8eefa, #2f4b67); | ||
| 79 | color: light-dark(black, white); | ||
| 80 | max-width: 34rem; | ||
| 81 | margin: 0 auto; | ||
| 82 | padding: 2rem 1rem 4rem; | ||
| 83 | }} | ||
| 84 | h1, h2, a {{ color: light-dark(#1a46cd, #938cff); }} | ||
| 85 | h1 {{ text-align: center; font-weight: normal; }} | ||
| 86 | a {{ text-decoration: dotted underline; }} | ||
| 87 | a:hover {{ text-decoration: underline; }} | ||
| 88 | .who {{ opacity: 0.6; font-size: 0.9rem; margin-top: -0.5rem; }} | ||
| 89 | .domain {{ opacity: 0.5; font-size: 0.85rem; }} | ||
| 90 | ul {{ list-style: "\\2744\\FE0E "; padding-left: 1.5rem; }} | ||
| 91 | li {{ margin: 0.4rem 0; }} | ||
| 92 | footer {{ margin-top: 3rem; opacity: 0.7; font-size: 0.9rem; text-align: center; }} | ||
| 93 | </style> | ||
| 94 | </head> | ||
| 95 | <body> | ||
| 96 | <h1>&#x2744;&#xFE0E; apps on {DOMAIN}</h1> | ||
| 97 | <main> | ||
| 98 | {body} | ||
| 99 | </main> | ||
| 100 | <footer> | ||
| 101 | <p>signed in with <a href="https://auth.{DOMAIN}/realms/master/account">snow sign on</a> — | ||
| 102 | one account for everything here.</p> | ||
| 103 | <p>missing access? contact clover.</p> | ||
| 104 | </footer> | ||
| 105 | </body> | ||
| 106 | </html>""") | ||