R-498: the app page's first steps name the household's address, not wiki.DOMAIN
AppInfo.WithAddress fills the catalog placeholder (<label>.DOMAIN -> the effective subdomain + customer domain, bare DOMAIN -> the domain) on a copy; appDetailHandler applies it. Pinned per catalog template (both languages) and on the rendered page. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
package stacks
|
||||
|
||||
import "regexp"
|
||||
|
||||
// reDomainPlaceholder matches the catalog's address placeholder in app copy: a bare `DOMAIN` or a
|
||||
// `<label>.DOMAIN` (wiki.DOMAIN, https://paste.DOMAIN/…). `\b` keeps SUBDOMAIN and MAIL_DOMAIN out —
|
||||
// the letter or underscore before them is a word character, so no boundary falls inside them.
|
||||
var reDomainPlaceholder = regexp.MustCompile(`(?:([a-z0-9-]+)\.)?\bDOMAIN\b`)
|
||||
|
||||
// WithAddress returns a copy of the app copy with the catalog's `DOMAIN` placeholder filled in
|
||||
// (R-498): `<label>.DOMAIN` becomes `<subdomain>.<domain>` — the app's EFFECTIVE subdomain (the one
|
||||
// the household chose at install), or the catalog label when none is known — and a bare `DOMAIN`
|
||||
// becomes `<domain>`. With no domain configured the copy is returned unchanged: a placeholder is
|
||||
// better than an address that is wrong. The receiver is not modified (slices are copied), because the
|
||||
// stack's metadata is shared with every other page. Pinned by TestR498_* (stacks and web).
|
||||
func (ai AppInfo) WithAddress(subdomain, domain string) AppInfo {
|
||||
if domain == "" {
|
||||
return ai
|
||||
}
|
||||
fill := func(s string) string {
|
||||
return reDomainPlaceholder.ReplaceAllStringFunc(s, func(m string) string {
|
||||
sub := reDomainPlaceholder.FindStringSubmatch(m)
|
||||
if sub[1] == "" {
|
||||
return domain
|
||||
}
|
||||
label := sub[1]
|
||||
if subdomain != "" {
|
||||
label = subdomain
|
||||
}
|
||||
return label + "." + domain
|
||||
})
|
||||
}
|
||||
list := func(in []string) []string {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, len(in))
|
||||
for i, s := range in {
|
||||
out[i] = fill(s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
out := ai
|
||||
out.Tagline = fill(ai.Tagline)
|
||||
out.UseCases = list(ai.UseCases)
|
||||
out.FirstSteps = list(ai.FirstSteps)
|
||||
out.Prerequisites = list(ai.Prerequisites)
|
||||
out.DefaultCreds = fill(ai.DefaultCreds)
|
||||
out.AddPeople = fill(ai.AddPeople)
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// R-498: the catalog's `<label>.DOMAIN` becomes the household's address; SUBDOMAIN / MAIL_DOMAIN are
|
||||
// not placeholders and stay; the receiver is not mutated; no domain → unchanged.
|
||||
func TestR498_WithAddressFillsThePlaceholder(t *testing.T) {
|
||||
in := AppInfo{
|
||||
FirstSteps: []string{"Nyisd meg a wiki.DOMAIN címet a böngészőben", "Lásd https://paste.DOMAIN/admin"},
|
||||
Prerequisites: []string{"SUBDOMAIN és MAIL_DOMAIN marad", "csak DOMAIN"},
|
||||
DefaultCreds: "admin@DOMAIN",
|
||||
}
|
||||
keep := AppInfo{FirstSteps: append([]string(nil), in.FirstSteps...), Prerequisites: append([]string(nil), in.Prerequisites...), DefaultCreds: in.DefaultCreds}
|
||||
|
||||
got := in.WithAddress("konyvtar", "example.hu")
|
||||
want := []string{"Nyisd meg a konyvtar.example.hu címet a böngészőben", "Lásd https://konyvtar.example.hu/admin"}
|
||||
if !reflect.DeepEqual(got.FirstSteps, want) {
|
||||
t.Errorf("FirstSteps = %q, want %q", got.FirstSteps, want)
|
||||
}
|
||||
if got.Prerequisites[0] != "SUBDOMAIN és MAIL_DOMAIN marad" || got.Prerequisites[1] != "csak example.hu" {
|
||||
t.Errorf("Prerequisites = %q", got.Prerequisites)
|
||||
}
|
||||
if got.DefaultCreds != "admin@example.hu" {
|
||||
t.Errorf("DefaultCreds = %q", got.DefaultCreds)
|
||||
}
|
||||
if !reflect.DeepEqual(in.FirstSteps, keep.FirstSteps) || !reflect.DeepEqual(in.Prerequisites, keep.Prerequisites) {
|
||||
t.Error("WithAddress mutated the shared metadata")
|
||||
}
|
||||
// No effective subdomain known: the catalog label stays.
|
||||
if g := in.WithAddress("", "example.hu").FirstSteps[0]; g != "Nyisd meg a wiki.example.hu címet a böngészőben" {
|
||||
t.Errorf("label fallback = %q", g)
|
||||
}
|
||||
// No domain: unchanged.
|
||||
if g := in.WithAddress("wiki", "").FirstSteps[0]; g != in.FirstSteps[0] {
|
||||
t.Errorf("no domain must leave the copy unchanged, got %q", g)
|
||||
}
|
||||
}
|
||||
|
||||
// R-498, per template: every catalog template's app copy, in both languages, renders with no literal
|
||||
// DOMAIN left. Reads the sibling catalog clone; skipped when it is not checked out next to this repo.
|
||||
func TestR498_EveryCatalogTemplateRendersAnAddress(t *testing.T) {
|
||||
root := filepath.Join("..", "..", "..", "..", "app-catalog-felhom.eu", "templates")
|
||||
entries, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
t.Skipf("catalog clone not found at %s: %v", root, err)
|
||||
}
|
||||
literal := regexp.MustCompile(`\bDOMAIN\b`)
|
||||
checked, withPlaceholder := 0, 0
|
||||
for _, e := range entries {
|
||||
dir := filepath.Join(root, e.Name())
|
||||
if _, err := os.Stat(filepath.Join(dir, ".felhom.yml")); err != nil {
|
||||
continue
|
||||
}
|
||||
st := Stack{Name: e.Name(), Meta: LoadMetadata(dir)}
|
||||
for _, lang := range []string{"hu", "en"} {
|
||||
ai := LocalizeStack(st, lang).Meta.AppInfo
|
||||
before := append(append(append([]string{ai.Tagline, ai.DefaultCreds, ai.AddPeople}, ai.UseCases...), ai.FirstSteps...), ai.Prerequisites...)
|
||||
for _, s := range before {
|
||||
if literal.MatchString(s) {
|
||||
withPlaceholder++
|
||||
break
|
||||
}
|
||||
}
|
||||
out := ai.WithAddress(st.Meta.Subdomain, "example.hu")
|
||||
for _, s := range append(append(append([]string{out.Tagline, out.DefaultCreds, out.AddPeople}, out.UseCases...), out.FirstSteps...), out.Prerequisites...) {
|
||||
if literal.MatchString(s) {
|
||||
t.Errorf("%s (%s): a literal DOMAIN is left: %q", e.Name(), lang, s)
|
||||
}
|
||||
}
|
||||
checked++
|
||||
}
|
||||
}
|
||||
if checked == 0 || withPlaceholder == 0 {
|
||||
t.Fatalf("the sweep proved nothing: %d renders checked, %d carried the placeholder", checked, withPlaceholder)
|
||||
}
|
||||
t.Logf("%d template renders checked, %d carried the placeholder", checked, withPlaceholder)
|
||||
}
|
||||
@@ -91,3 +91,24 @@ func TestAutoUpdateLine_PageSaysItOnce(t *testing.T) {
|
||||
t.Error("the line must go once the app no longer runs what the automatic step installed")
|
||||
}
|
||||
}
|
||||
|
||||
// R-498: the app page's „Első lépések" shows the household's address, never the catalog's wiki.DOMAIN.
|
||||
func TestR498_AppPageFirstStepsShowTheRealAddress(t *testing.T) {
|
||||
s, _ := credsHarness(t)
|
||||
sd := filepath.Join(s.cfg.Paths.StacksDir, "crafty")
|
||||
meta := "display_name: Crafty\nslug: crafty\nsubdomain: wiki\napp_info:\n first_steps:\n - 'Nyisd meg a wiki.DOMAIN címet a böngészőben'\n"
|
||||
if err := os.WriteFile(filepath.Join(sd, ".felhom.yml"), []byte(meta), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = s.stackMgr.ScanStacks()
|
||||
s.loadTemplates()
|
||||
rr := httptest.NewRecorder()
|
||||
s.appDetailHandler(rr, httptest.NewRequest("GET", "/apps/crafty", nil), "crafty")
|
||||
page := html.UnescapeString(rr.Body.String())
|
||||
if !strings.Contains(page, "Nyisd meg a wiki.example.hu c") {
|
||||
t.Fatalf("the first step must name the real address wiki.example.hu (status %d)", rr.Code)
|
||||
}
|
||||
if strings.Contains(page, "wiki.DOMAIN") {
|
||||
t.Error("the literal wiki.DOMAIN reached the page")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -768,7 +768,8 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
|
||||
data := s.baseData("stacks", found.Meta.DisplayName)
|
||||
data["Stack"] = found
|
||||
data["Meta"] = found.Meta
|
||||
data["AppInfo"] = found.Meta.AppInfo
|
||||
// R-498: the catalog copy says `wiki.DOMAIN`; the household needs its own address.
|
||||
data["AppInfo"] = found.Meta.AppInfo.WithAddress(effectiveSubdomain, s.cfg.Customer.Domain)
|
||||
// v0.279.0 (decision 45): the default-login card only while that login is in effect.
|
||||
data["KnownLoginLine"] = s.knownLoginLine(s.langFor(r), &found.Meta, found.AppConfig, found.Deployed)
|
||||
data["DefaultLoginReplaced"] = defaultLoginReplaced(&found.Meta, found.AppConfig, found.Deployed)
|
||||
|
||||
Reference in New Issue
Block a user