diff --git a/CLAUDE.md b/CLAUDE.md
index 9783b39..d935591 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -3,9 +3,8 @@
Flask dashboard aggregating status from the user's self-hosted media stack
— Sonarr, Radarr, SABnzbd, Tautulli, Prowlarr, Ombi — plus Unraid array
stats and a Rotten Tomatoes "what's popular that I don't have yet" feed.
-Branded "Media Centre" — a single neutral, professional dark theme (the
-earlier five novelty themes were removed). Runs as one Docker container on
-the user's Unraid home server. Server-side project directory is still named
+Branded "Greendale Media Centre." Runs as one Docker container on the
+user's Unraid home server. Server-side project directory is named
`greendale`; the container/repo are named `arr-summary`.
## Architecture
@@ -34,20 +33,10 @@ the user's Unraid home server. Server-side project directory is still named
`BeautifulSoup`, JSON-LD parsing), not via an official API — brittle
to upstream HTML changes by nature.
- **`templates/`** — Jinja2 templates (`dashboard.html`, `login.html`,
- `settings.html`). Stale duplicates of these once sat at the repo root;
- they were never rendered (Flask loads from `templates/`) and were deleted
- during the redesign.
- - All three share one token set defined on `:root` — surfaces, a 6-step
- `--fs-*` type scale, semantic colours, and `--tap: 44px` for minimum
- touch targets. Add new UI with those tokens rather than literal values.
- - `dashboard.html` is ordered by urgency, deliberately mirroring the cache
- tiers in `app.py`: a "Right now" strip (live streams, download queue,
- array usage) sits above the fold, with library, discovery, requests and
- watch insights below it.
- - Degraded services render through the `service_error()` macro, which
- pairs `friendly_error` (human cause) with the raw exception behind a
- disclosure, passed through `redact_secrets` — SABnzbd and Tautulli put
- their API key in the request URL, so it appears in raw tracebacks.
+ `settings.html`); duplicated at the top level too (`dashboard.html` etc.
+ outside `templates/`) — appears to be a leftover from an earlier project
+ layout or the `export-to-unraid.sh` tooling. Left as-is rather than
+ restructured without confirming with the user first.
- **`export-to-unraid.sh`** — a pre-existing standalone deploy script
(build image locally → tarball → rsync + `docker load` over SSH) that
predates the Forgejo pipeline below. Still present, not removed, but the
@@ -89,5 +78,5 @@ pipeline mechanics. Push to `main` on the `forgejo` remote
and `LOGIN_PASSWORD` — this project was already doing this correctly
before the Forgejo migration (compose file uses `${VAR}` substitution from
`.env`, no hardcoded secrets), unlike `n64-tracker` which needed fixing.
-`settings.json` (timezone, changed login password) also persists
+`settings.json` (timezone, theme, changed login password) also persists
server-side outside git.
diff --git a/app.py b/app.py
index e95eb4b..0d44548 100644
--- a/app.py
+++ b/app.py
@@ -1001,6 +1001,7 @@ def dashboard():
s = load_settings()
timezone = s.get("timezone", "America/New_York")
+ theme = s.get("theme", "community")
# Public-facing URLs for card header links (via NPM reverse proxy)
arr_host_public = f"https://hippofam.com"
service_urls = {
@@ -1019,6 +1020,7 @@ def dashboard():
rt_movies=rt_movies, rt_tv=rt_tv,
sonarr_titles=sonarr_titles, radarr_titles=radarr_titles,
timezone=timezone,
+ theme=theme,
service_urls=service_urls,
)
@@ -1241,6 +1243,7 @@ def settings_page():
prowlarr_api_key=PROWLARR_API_KEY,
ombi_api_key=OMBI_API_KEY,
arr_host=os.environ.get("ARR_HOST", ""),
+ theme=s.get("theme", "community"),
)
@@ -1249,7 +1252,7 @@ def settings_page():
def api_save_settings():
data = request.get_json()
allowed = {"timezone", "sonarr_api_key", "radarr_api_key", "sabnzbd_api_key",
- "tautulli_api_key", "prowlarr_api_key", "ombi_api_key", "arr_host"}
+ "tautulli_api_key", "prowlarr_api_key", "ombi_api_key", "arr_host", "theme"}
filtered = {k: v for k, v in data.items() if k in allowed}
save_settings(filtered)
return jsonify({"success": True})
@@ -1261,6 +1264,7 @@ def api_get_settings():
s = load_settings()
return jsonify({
"timezone": s.get("timezone", "America/New_York"),
+ "theme": s.get("theme", "community"),
})
diff --git a/dashboard.html b/dashboard.html
new file mode 100644
index 0000000..46e2979
--- /dev/null
+++ b/dashboard.html
@@ -0,0 +1,1964 @@
+
+
+
+ {% if up and tautulli.sessions %}
+ {% for s in tautulli.sessions %}
+
+ {{ '🎬' if s.media_type == 'movie' else '📺' }}
+
+
{{ s.title }}
+
{{ s.user }}
+
+
+ {% endfor %}
+ {% else %}
+
No active streams
+ {% endif %}
+
+
+
+
+
+
—
+
+ {% if theme == 'office' %}Dunder Mifflin HQ
+ {% elif theme == 'parks' %}Pawnee City Hall
+ {% elif theme == 'potter' %}Hogwarts Great Hall
+ {% elif theme == 'starwars' %}Death Star Control Room
+ {% else %}Study Room F{% endif %}
+ · ⚙ Settings
+
+
+
+
+
+
+
+ {% if theme == 'office' %}
+ 📋 Monday morning meeting: checking downloads, missing episodes, and who took the last of the coffee.
+ {% elif theme == 'parks' %}
+ 📋 Today's agenda: checking what's missing, what's downloading, and making Pawnee great again.
+ {% elif theme == 'potter' %}
+ 📋 Today's scroll: checking what spells are missing, what's being conjured, and what deserves a watch.
+ {% elif theme == 'starwars' %}
+ 📋 Rebel briefing: checking what's missing, what's downloading, and what the Force recommends watching.
+ {% else %}
+ 📋 Today's agenda: checking what's missing, what's downloading, and what deserves to be watched.
+ {% endif %}
+
- {% set mt = item.media_type | lower %}
- {{ 'TV' if mt == 'episode' else ('Movie' if mt == 'movie' else mt or '?') }}
- {{ item.plays }}×
-
- {% endfor %}
-
- {% else %}
-
No play history yet.
- {% endif %}
-
-
-
-
-
Top viewers
-
- {% if tautulli.top_users %}
-
- {% for u in tautulli.top_users %}
-
- {{ loop.index }}
-
-
{{ u.username }}
-
- {{ u.plays }} plays
-
- {% endfor %}
-
- {% else %}
-
No viewer data yet.
- {% endif %}
-
-
-
-
-
- {% if tautulli.top_tv_list or tautulli.top_movie_list %}
-
-
-
Top TV
-
- {% if tautulli.top_tv_list %}
-
- {% for item in tautulli.top_tv_list %}
-
- {{ loop.index }}
-
{{ item.title }}
- {{ item.plays }} plays
-
- {% endfor %}
-
- {% else %}
No data yet.
{% endif %}
-
-
-
-
Top movies
-
- {% if tautulli.top_movie_list %}
-
- {% for item in tautulli.top_movie_list %}
-
- {{ loop.index }}
-
{{ item.title }}
- {{ item.plays }} plays
-
- {% endfor %}
-
- {% else %}
No data yet.
{% endif %}
-
-
-
+
+ {% if theme == 'office' %}
+ 📋 Monday morning meeting: checking downloads, missing episodes, and who took the last of the coffee.
+ {% elif theme == 'parks' %}
+ 📋 Today's agenda: checking what's missing, what's downloading, and making Pawnee great again.
+ {% elif theme == 'potter' %}
+ 📋 Today's scroll: checking what spells are missing, what's being conjured, and what deserves a watch.
+ {% elif theme == 'starwars' %}
+ 📋 Rebel briefing: checking what's missing, what's downloading, and what the Force recommends watching.
+ {% else %}
+ 📋 Today's agenda: checking what's missing, what's downloading, and what deserves to be watched.
{% endif %}
-
- {% endif %}
+