config: name the instance, not just the profile
One name for the server, on the browser tab and on the phone,
read from PRO_TACTS_INSTANCE_NAME, since the profile's variable was outgrown;
rake dev calls itself "pro-tacts (dev)" unless you export a name of your own,
and a deployment still setting PROFILE_NAME falls back to "pro-tacts" alone.

Assisted-by: Claude Opus 5 via Claude Code
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
change ynqlxnsqtvlzzxyqtwmyzzvoqwkrvkzs
commit a76a54ba813805a79242ac3dc51559cc0aa19fcd
author Alpha Chen <alpha@kejadlen.dev>
date
parent yuvpusvv
diff --git a/Rakefile b/Rakefile
index 36bf003..f757efe 100644
--- a/Rakefile
+++ b/Rakefile
@@ -28,6 +28,7 @@ task :dev do
   # log/exchange.log stays the deployment default. An exported
   # 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)"
   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 b342e26..5c1430a 100644
--- a/lib/pro_tacts/admin/layout.rb
+++ b/lib/pro_tacts/admin/layout.rb
@@ -49,7 +49,9 @@ module ProTacts
           head do
             meta(charset: "utf-8")
             meta(name: "viewport", content: "width=device-width, initial-scale=1")
-            title { "pro-tacts — #{@title}" }
+            # The instance's name, so a tab says which server it is
+            # (`rake dev` names its own).
+            title { "#{ProTacts.config.instance_name} — #{@title}" }
             # 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 335a12f..3ff9e03 100644
--- a/lib/pro_tacts/config.rb
+++ b/lib/pro_tacts/config.rb
@@ -62,15 +62,13 @@ module ProTacts
       Pathname.new(@env.fetch("PRO_TACTS_EXCHANGE_LOG", data_dir / "log/exchange.log"))
     end
 
-    # The CardDAV account's display name, PRO_TACTS_PROFILE_NAME — what
-    # a device labels the account it installs, and the one way to tell
-    # two pro-tacts accounts apart on the same phone. nil when unset
-    # rather than a default: the name belongs to the profile, so
-    # ProTacts::Profile::DEFAULT_NAME is the fallback and this reports
-    # only the override.
-    #: () -> String?
-    def profile_name
-      @env.fetch("PRO_TACTS_PROFILE_NAME", nil)
+    # What this running server calls itself, PRO_TACTS_INSTANCE_NAME:
+    # the CardDAV account a device installs and the browser tab both
+    # carry it, so two pro-tacts servers can be told apart on the same
+    # phone or in the same window.
+    #: () -> String
+    def instance_name
+      @env.fetch("PRO_TACTS_INSTANCE_NAME", "pro-tacts")
     end
 
     # The running image's version, baked in as a build arg (see
diff --git a/lib/pro_tacts/profile.rb b/lib/pro_tacts/profile.rb
index b699566..960885c 100644
--- a/lib/pro_tacts/profile.rb
+++ b/lib/pro_tacts/profile.rb
@@ -32,20 +32,6 @@ module ProTacts
   class Profile
     IDENTIFIER_PREFIX = "dev.kejadlen.pro-tacts.carddav"
 
-    # The account name when the environment names none. A constant
-    # because the install screen states the name the download is about
-    # to carry and must not restate it as a second literal.
-    DEFAULT_NAME = "pro-tacts"
-
-    # The name every render and the install screen state. A method
-    # rather than a render parameter because there is only one source
-    # for it: a caller passing a name of its own would be labelling an
-    # account no device installs.
-    #: () -> String
-    def self.account_name
-      ProTacts.config.profile_name || DEFAULT_NAME
-    end
-
     # The server ignores the username and password: identity comes from
     # the header the proxy in front of it writes (see
     # ProTacts::ProxyAuth). /setup passes the requester's login anyway,
@@ -54,7 +40,9 @@ module ProTacts
     # and dropping it is untested.
     #: (hostname: String, username: String) -> String
     def self.render(hostname:, username:)
-      name = account_name
+      # The instance's name rather than a render parameter: a caller
+      # passing its own would label an account no device installs.
+      name = ProTacts.config.instance_name
       identifier = "#{IDENTIFIER_PREFIX}.#{host_digest(hostname)}.#{unique_hex}"
 
       builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |x|
diff --git a/lib/pro_tacts/web/setup.rb b/lib/pro_tacts/web/setup.rb
index c8345fc..424186a 100644
--- a/lib/pro_tacts/web/setup.rb
+++ b/lib/pro_tacts/web/setup.rb
@@ -23,7 +23,7 @@ module ProTacts
       r.is do
         r.get do
           response["Content-Type"] = "text/html; charset=utf-8"
-          Admin::DeviceSetup.call(hostname: r.host, name: Profile.account_name)
+          Admin::DeviceSetup.call(hostname: r.host, name: ProTacts.config.instance_name)
         end
       end
 
diff --git a/test/pro_tacts/admin/test_device_setup.rb b/test/pro_tacts/admin/test_device_setup.rb
index 777fd5b..2fdd066 100644
--- a/test/pro_tacts/admin/test_device_setup.rb
+++ b/test/pro_tacts/admin/test_device_setup.rb
@@ -39,7 +39,7 @@ class AdminDeviceSetupTest < Minitest::Test
       assert_equal 200, last_response.status
       assert_equal "text/html; charset=utf-8", last_response["Content-Type"]
       assert_includes last_response.body, "box.example.ts.net"
-      assert_includes last_response.body, ProTacts::Profile::DEFAULT_NAME
+      assert_includes last_response.body, %(<dd class="type-body-sm">pro-tacts</dd>)
       assert_includes last_response.body, "/setup/carddav.mobileconfig"
     end
   end
@@ -48,7 +48,7 @@ class AdminDeviceSetupTest < Minitest::Test
   # reaching only one of the two would put a name on the screen that the
   # installed account does not have.
   def test_the_configured_name_reaches_the_screen_and_the_document
-    with_config("PRO_TACTS_PROFILE_NAME" => "pro-tacts (dev)") do
+    with_config("PRO_TACTS_INSTANCE_NAME" => "pro-tacts (dev)") do
       get "/setup"
 
       assert_includes last_response.body, %(<dd class="type-body-sm">pro-tacts (dev)</dd>)
diff --git a/test/pro_tacts/admin/test_footer.rb b/test/pro_tacts/admin/test_footer.rb
index c4cc8ef..6cd0e09 100644
--- a/test/pro_tacts/admin/test_footer.rb
+++ b/test/pro_tacts/admin/test_footer.rb
@@ -82,4 +82,14 @@ class AdminFooterTest < Minitest::Test
 
     refute_includes last_response.body, "data-server"
   end
+
+  def test_the_title_names_the_instance
+    with_env({})
+
+    assert_includes last_response.body, "<title>pro-tacts — Device setup</title>"
+
+    with_env("PRO_TACTS_INSTANCE_NAME" => "pro-tacts (dev)")
+
+    assert_includes last_response.body, "<title>pro-tacts (dev) — Device setup</title>"
+  end
 end
diff --git a/test/pro_tacts/test_config.rb b/test/pro_tacts/test_config.rb
index 8dc0a5a..d2d0102 100644
--- a/test/pro_tacts/test_config.rb
+++ b/test/pro_tacts/test_config.rb
@@ -58,12 +58,11 @@ class ConfigTest < Minitest::Test
     assert_equal Pathname.new("stderr"), ProTacts::Config.new("PRO_TACTS_EXCHANGE_LOG" => "stderr").exchange_log_path
   end
 
-  # nil rather than a default, so the fallback stays Profile's alone.
-  def test_profile_name_is_nil_when_unset
-    assert_nil ProTacts::Config.new({}).profile_name
+  def test_instance_name_defaults_to_pro_tacts
+    assert_equal "pro-tacts", ProTacts::Config.new({}).instance_name
   end
 
-  def test_profile_name_is_passed_through
-    assert_equal "pro-tacts (dev)", ProTacts::Config.new("PRO_TACTS_PROFILE_NAME" => "pro-tacts (dev)").profile_name
+  def test_instance_name_is_overridable
+    assert_equal "pro-tacts (dev)", ProTacts::Config.new("PRO_TACTS_INSTANCE_NAME" => "pro-tacts (dev)").instance_name
   end
 end
diff --git a/test/pro_tacts/test_profile.rb b/test/pro_tacts/test_profile.rb
index 004b2aa..15f6b7f 100644
--- a/test/pro_tacts/test_profile.rb
+++ b/test/pro_tacts/test_profile.rb
@@ -45,7 +45,7 @@ class ProfileTest < Minitest::Test
   end
 
   def test_name_can_be_overridden
-    with_config("PRO_TACTS_PROFILE_NAME" => "pro-tacts (dev)") do
+    with_config("PRO_TACTS_INSTANCE_NAME" => "pro-tacts (dev)") do
       xml = render
 
       assert_match(%r{<key>CardDAVAccountDescription</key>\s*<string>pro-tacts \(dev\)</string>}, xml)