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)
|
||||
}
|
||||
Reference in New Issue
Block a user