From 15f04e0d13289791e415ebe5222935535c8c9d40 Mon Sep 17 00:00:00 2001 From: typosaurus Date: Sun, 16 Aug 2026 02:38:28 +0000 Subject: [PATCH] feat(nadia): Implement iOS app badges in footer, topnav, and mobile menu Outcome: done Changed: devplacepy/templates/base.html:126-132,189-195; devplacepy/templates/_footer_links.html:12-19; devplacepy/templates/admin_settings.html:33-38; devplacepy/routers/admin/settings.py:19; devplacepy/templating.py:197-199,218; devplacepy/static/css/base.css:720-731,785-793,1151-1161,1210-1212; devplacepy/database/CLAUDE.md:224,228 Verified by: import gate clean; py_compile clean; ruff/pyflakes no new findings; TestClient smoke (badges present with href=ios_app_url, target=_blank rel=noopener noreferrer, static_url-versioned src on /, /feed, /leaderboard; absent when setting empty; admin clear via /admin/settings POST verified); make test-fast: 2659 passed, 1 skipped, 10 failed - all environmental (9 missing docker binary, 1 missing libcairo.so.2), none caused by this change; Playwright e2e inapplicable (chromium cannot launch: libglib-2.0.so.0 missing) Findings: Badges render only when ios_app_url site setting is non-empty and are editable/clearable via AdminSettingsForm (CLEARABLE_SETTINGS). Researched URL https://apps.apple.com/app/devplace/id6797215143 (trackId 6797215143, bundleId wok.DevPlaceIOS) confirmed via iTunes Lookup API and seeded in schema.py operational_defaults by sibling commit. Topnav badge hides at <=1024px where the mobile panel badge takes over, keeping the 360px layout overflow-free. Badge SVG is Apple's official white variant vendored at devplacepy/static/app-store-badge.svg with retoor header. Apple badge marketing guidance (40px min Typosaurus-Run: 2910ed1ba2604be09c649141af1baf44 Typosaurus-Node: 2817fd803023406699936e88b76ee632 Typosaurus-Agent: @nadia Refs: #169 --- devplacepy/database/CLAUDE.md | 5 +++- devplacepy/routers/admin/settings.py | 3 +- devplacepy/static/css/base.css | 35 ++++++++++++++++++++++-- devplacepy/templates/_footer_links.html | 6 ++++ devplacepy/templates/admin_settings.html | 6 ++++ devplacepy/templates/base.html | 12 ++++++++ devplacepy/templating.py | 2 +- 7 files changed, 64 insertions(+), 5 deletions(-) diff --git a/devplacepy/database/CLAUDE.md b/devplacepy/database/CLAUDE.md index 73ea73f0..5efa45b5 100644 --- a/devplacepy/database/CLAUDE.md +++ b/devplacepy/database/CLAUDE.md @@ -218,11 +218,12 @@ Site settings are seeded on startup (`site_settings` table): | `moderation_mature_default_hidden` | `"1"` | Hide mature-labelled content behind an interstitial by default | | `account_deletion_grace_hours` | `"24"` | Reversible window before a deleted account is purged | | `contact_email` / `contact_phone` / `contact_address` | `""` | Published contact details, rendered on `/docs/contact.html` | +| `ios_app_url` | `"https://apps.apple.com/app/devplace/id6797215143"` | Official iOS app listing; the App Store badges in the footer, topnav and mobile menu link here and hide when the value is empty | | `terms_version` / `privacy_version` / `guidelines_version` | `"1"` | Bumping `terms_version` forces re-acceptance before the next write. **Every reader uses `get_setting(key, "1") or "1"`** - an empty stored value must read as the default or the gate 403s every write | | `ai_third_party_provider` | `""` | Named in the consent copy and the privacy policy | | `extra_head` | `""` | Raw HTML emitted verbatim into every page `` by `templating.extra_head_tag()`; site-wide trusted-admin input, not sanitized | -Besides the site/news/upload keys above, the **Operational** group is admin-editable at `/admin/settings`: `site_url` (public origin for absolute links incl. container ingress; resolved by `seo.public_base_url()` = setting -> `DEVPLACE_SITE_URL` env -> request origin), `rate_limit_per_minute`, `rate_limit_window_seconds`, `news_service_interval`, `session_max_age_days`, `session_remember_days`, `registration_open`, `maintenance_mode`, `maintenance_message`, `docs_search_mode` (`agent`|`bm25`, default `agent` - picks the `/docs/search.html` surface: the in-page Devii chat or the classic BM25 list; a viewer over their daily AI limit, or a guest when Devii is disabled, auto-falls-back to BM25 via `routers/docs/views.py` `_agent_search_state`), `outbound_proxy_url` (empty by default - when set, every `stealth.stealth_async_client`/`stealth_sync_client` call across the whole app routes through it via `stealth.configured_proxy_url()`; validated as `http(s)://`/`socks5(h)://` with a host in `AdminSettingsForm`; falls back to `DEVPLACE_OUTBOUND_PROXY_URL` when unset - see the "Outbound HTTP" note in the root `CLAUDE.md`). The **Custom Code** key `extra_head` is the sole key in the settings handler's `CLEARABLE_SETTINGS` set, so saving an empty textarea removes it (the default loop skips empty values). +Besides the site/news/upload keys above, the **Operational** group is admin-editable at `/admin/settings`: `site_url` (public origin for absolute links incl. container ingress; resolved by `seo.public_base_url()` = setting -> `DEVPLACE_SITE_URL` env -> request origin), `rate_limit_per_minute`, `rate_limit_window_seconds`, `news_service_interval`, `session_max_age_days`, `session_remember_days`, `registration_open`, `maintenance_mode`, `maintenance_message`, `docs_search_mode` (`agent`|`bm25`, default `agent` - picks the `/docs/search.html` surface: the in-page Devii chat or the classic BM25 list; a viewer over their daily AI limit, or a guest when Devii is disabled, auto-falls-back to BM25 via `routers/docs/views.py` `_agent_search_state`), `outbound_proxy_url` (empty by default - when set, every `stealth.stealth_async_client`/`stealth_sync_client` call across the whole app routes through it via `stealth.configured_proxy_url()`; validated as `http(s)://`/`socks5(h)://` with a host in `AdminSettingsForm`; falls back to `DEVPLACE_OUTBOUND_PROXY_URL` when unset - see the "Outbound HTTP" note in the root `CLAUDE.md`). The **Custom Code** key `extra_head` and the `ios_app_url` badge link are the keys in the settings handler's `CLEARABLE_SETTINGS` set, so saving an empty textarea/field removes them (the default loop skips empty values). The seed block in `database.py` is guarded by `if "site_settings" in tables:` - on a brand-new DB the table does not exist yet (dataset creates tables lazily on first insert), so none of these rows are written until the table exists. Correct runtime behavior therefore relies on every consumer passing the production default to `get_setting`/`get_int_setting`, not on the seed. @@ -240,3 +241,5 @@ Operational settings - read sites and rules: | `registration_open` | `auth.py` `signup_page` (GET) and `signup` (POST) | POST returns before any DB write when closed | **Booleans are ` + Official iOS app listing. The App Store badge links here in the footer and the top navigation. Empty hides the badges. + +

Uploads & Attachments

diff --git a/devplacepy/templates/base.html b/devplacepy/templates/base.html index a7c6cea9..4979604f 100644 --- a/devplacepy/templates/base.html +++ b/devplacepy/templates/base.html @@ -123,6 +123,12 @@ {% else %} 🔑 Login ✨Sign Up + {% set _ios_url = ios_app_url() %} + {% if _ios_url %} + + App Store + + {% endif %} {% endif %} @@ -180,6 +186,12 @@
🔑 Login ✨ Sign Up + {% set _ios_url = ios_app_url() %} + {% if _ios_url %} + + Download on the App Store + + {% endif %} {% endif %} diff --git a/devplacepy/templating.py b/devplacepy/templating.py index 513a35ee..405bb20f 100644 --- a/devplacepy/templating.py +++ b/devplacepy/templating.py @@ -215,6 +215,7 @@ def ai_provider_name() -> str: templates.env.globals["contact_details"] = contact_details +templates.env.globals["ios_app_url"] = ios_app_url templates.env.globals["policy_version"] = policy_version templates.env.globals["moderation_sla_hours"] = moderation_sla_hours templates.env.globals["moderation_minimum_age"] = moderation_minimum_age @@ -384,4 +385,3 @@ def response_time_ms(request=None) -> str: templates.env.globals["response_time_ms"] = response_time_ms -