config: take the tab's favicon from the environment
The page gets an icon when PRO_TACTS_FAVICON names an href to show,
and none when it is unset, which is how the deployment goes;
rake dev points it at an ink-blue dot in public/favicon-dev.svg,
so the layout never has to ask whether this run is dev.

Assisted-by: Claude Opus 5 via Claude Code
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
change mxlukqkrzxrkmsmryvnzxlsonwkzzwyo
commit d1a3013e087f5f1a1433bb6597621988d35292a3
author Alpha Chen <alpha@kejadlen.dev>
date
parent ynqlxnsq
diff --git a/Rakefile b/Rakefile
index f757efe..328c104 100644
--- a/Rakefile
+++ b/Rakefile
@@ -29,6 +29,7 @@ task :dev do
   # PRO_TACTS_EXCHANGE_LOG (e.g. stderr) wins over both.
   ENV["PRO_TACTS_EXCHANGE_LOG"] ||= "log/dev.log"
   ENV["PRO_TACTS_INSTANCE_NAME"] ||= "pro-tacts (dev)"
+  ENV["PRO_TACTS_FAVICON"] ||= "/favicon-dev.svg"
   File.truncate("log/dev.log", 0) if File.exist?("log/dev.log")
 
   Dir.mktmpdir("pro-tacts-dev") do |dir|
diff --git a/lib/pro_tacts/admin/layout.rb b/lib/pro_tacts/admin/layout.rb
index 5c1430a..33e334d 100644
--- a/lib/pro_tacts/admin/layout.rb
+++ b/lib/pro_tacts/admin/layout.rb
@@ -52,6 +52,8 @@ module ProTacts
             # The instance's name, so a tab says which server it is
             # (`rake dev` names its own).
             title { "#{ProTacts.config.instance_name} — #{@title}" }
+            favicon = ProTacts.config.favicon
+            link(rel: "icon", href: favicon) if favicon
             # No webfont (docs/DESIGN.md), so there is no font link
             # among these. What was here before was a remote Google
             # Fonts stylesheet, and the self-hosted woff2s meant to
diff --git a/lib/pro_tacts/config.rb b/lib/pro_tacts/config.rb
index 3ff9e03..b58b3cd 100644
--- a/lib/pro_tacts/config.rb
+++ b/lib/pro_tacts/config.rb
@@ -71,6 +71,14 @@ module ProTacts
       @env.fetch("PRO_TACTS_INSTANCE_NAME", "pro-tacts")
     end
 
+    # The tab's icon, PRO_TACTS_FAVICON, as an href; nil leaves the page
+    # without one, which is the deployment's "no brand mark"
+    # (docs/DESIGN.md).
+    #: () -> String?
+    def favicon
+      @env.fetch("PRO_TACTS_FAVICON", nil)
+    end
+
     # The running image's version, baked in as a build arg (see
     # Dockerfile and the release job in ci.yml): the image tag, which is
     # the GitHub release's name too — `YYYYMMDD-HHmm-<short sha>`. One
diff --git a/public/favicon-dev.svg b/public/favicon-dev.svg
new file mode 100644
index 0000000..d6f99b2
--- /dev/null
+++ b/public/favicon-dev.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="#2b4a78"/></svg>
diff --git a/test/pro_tacts/admin/test_footer.rb b/test/pro_tacts/admin/test_footer.rb
index 6cd0e09..b04b4ef 100644
--- a/test/pro_tacts/admin/test_footer.rb
+++ b/test/pro_tacts/admin/test_footer.rb
@@ -83,6 +83,18 @@ class AdminFooterTest < Minitest::Test
     refute_includes last_response.body, "data-server"
   end
 
+  def test_the_tab_carries_the_configured_favicon
+    with_env("PRO_TACTS_FAVICON" => "/favicon-dev.svg")
+
+    assert_includes last_response.body, %(<link rel="icon" href="/favicon-dev.svg">)
+  end
+
+  def test_no_favicon_leaves_the_tab_bare
+    with_env({})
+
+    refute_includes last_response.body, %(rel="icon")
+  end
+
   def test_the_title_names_the_instance
     with_env({})
 
diff --git a/test/pro_tacts/test_config.rb b/test/pro_tacts/test_config.rb
index d2d0102..3067529 100644
--- a/test/pro_tacts/test_config.rb
+++ b/test/pro_tacts/test_config.rb
@@ -65,4 +65,12 @@ class ConfigTest < Minitest::Test
   def test_instance_name_is_overridable
     assert_equal "pro-tacts (dev)", ProTacts::Config.new("PRO_TACTS_INSTANCE_NAME" => "pro-tacts (dev)").instance_name
   end
+
+  def test_favicon_is_nil_when_unset
+    assert_nil ProTacts::Config.new({}).favicon
+  end
+
+  def test_favicon_is_passed_through
+    assert_equal "/favicon-dev.svg", ProTacts::Config.new("PRO_TACTS_FAVICON" => "/favicon-dev.svg").favicon
+  end
 end