v0.315.0: one app page with four tabs; the guide the catalog can carry; no Raspberry Pi badge
gates / gates (push) Failing after 47s

An app was six pages — details, install/settings, logs, export, the per-app backup panel and a
details card for moving data — and every "Vissza" on them went to the app LIST, so there was no way
back to the app you were looking at. It is now ONE page with four tab URLs under /apps/{slug}/,
one shared header and one set of banners.

  settings  Mukodes (Start/Restart/Stop, Stop confirms) + the cards that want something from the
            household + the install-time and optional settings. The default for an installed app,
            RENDERED at the bare URL and never redirected to.
  data      the data folders, move to another drive, the nightly copy, the 2nd-copy setting, export
  guide     tagline, screenshots, use cases, first steps, the catalog's guide, the FAQ, the docs
            link — and the WHOLE page for an app that is not installed
  logs      the log, and the only tab whose poller runs

The tabs are links, not panels: a link in an alert has to land ON a tab, and the log poller must not
run on the other three. Start/Restart/Stop left the page header, where they read as menu buttons.

The old /stacks/{name}/{deploy,logs,export,backup} GETs answer 302 — never 301, which a browser
caches beyond anyone's reach — to the tab that now holds them, query string carried through, with
name->slug resolved through the stack list rather than assumed equal. Every POST on those paths is
untouched: the backup form, the app-email toggle and the reveal endpoints keep their URLs.

A value asked AFTER the app starts is now generic (D2). The catalog marks a field `after_start: true`
and supplies its label; the ACTION is Go, in a registry keyed by slug + env var. The catalog can
never declare what runs — a declared field with no registered action renders nothing, POSTs 404 and
logs one WARN with the env var KEY, never the value. A red-proof found the second half of that guard
unpinned: deleting the "the catalog must have DECLARED this field" check left the suite green, and a
POST for an undeclared field then reached Plex and opened the setup gate.
TestAfterStart_RegistryAloneIsNotAnInvitation pins it.

New catalog block app_info.guide (sections + faq + checked_version), parsed, localised field by
field and rendered with the household's own address filled in. It renders only when checked_version
names the image tag its author clicked through. No template carries one yet.

The Pi and "csak x86" badges are gone; PiCompatible and the catalog key stay this release, or a box
on 0.314.0 would read "csak x86" on every app (R-1067).

template_id_gate.py now follows {{template}} includes, with a scope decoy proving the walk did not
become vacuous.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
This commit is contained in:
2026-10-15 18:37:50 +02:00
parent 85f65c70d8
commit e66d4669d6
120 changed files with 16239 additions and 9866 deletions
+26 -31
View File
@@ -401,43 +401,32 @@ func (s *Server) stacksHandler(w http.ResponseWriter, r *http.Request) {
s.executeTemplate(w, r, "stacks", data)
}
func (s *Server) logsHandler(w http.ResponseWriter, r *http.Request, name string) {
stack, ok := s.stackMgr.GetStack(name)
func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name string) {
data, ok := s.deployPageData(w, r, name)
if !ok {
http.NotFound(w, r)
return
}
logs, err := s.stackMgr.GetLogs(name, 200)
if err != nil {
logs = s.msg(r, "logs.fetch_failed", err)
}
// Raw mode: return plain text for AJAX polling
if r.URL.Query().Get("raw") == "1" {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprint(w, logs)
return
}
data := s.baseData("logs", stack.Meta.DisplayName+" — Naplók")
data["TitleKey"], data["TitleArgs"] = "page.title.logs", []interface{}{stack.Meta.DisplayName} // i18n: the Hungarian title above is what hu renders
data["Stack"] = stacks.LocalizeStackPtr(stack, s.langFor(r)) // R-560
data["Logs"] = logs
s.executeTemplate(w, r, "logs", data)
s.executeTemplate(w, r, "deploy", data)
}
func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name string) {
// deployPageData assembles the install/settings form for one stack: the generated values, the
// install-time fields, the optional settings, the integrations, the geo rule, the app-email toggle
// and (for a new install) the memory and headroom summaries. It writes a 404 and reports false when
// the stack is unknown.
//
// It serves TWO surfaces since v0.315.0 — the install page and the app page's settings tab — and the
// `AlreadyDeployed` flag it sets is what each of them branches on, exactly as the one page did.
func (s *Server) deployPageData(w http.ResponseWriter, r *http.Request, name string) (map[string]interface{}, bool) {
if s.isDebug() {
s.logger.Printf("[DEBUG] [web] deployHandler: stack=%s method=%s", name, r.Method)
s.logger.Printf("[DEBUG] [web] deployPageData: stack=%s method=%s", name, r.Method)
}
meta, appCfg, err := s.stackMgr.GetDeployFields(name)
if err != nil {
if s.isDebug() {
s.logger.Printf("[DEBUG] [web] deployHandler: stack=%s not found: %v", name, err)
s.logger.Printf("[DEBUG] [web] deployPageData: stack=%s not found: %v", name, err)
}
http.NotFound(w, r)
return
return nil, false
}
stack, _ := s.stackMgr.GetStack(name)
@@ -735,10 +724,16 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
data["FlashError"] = flashErr
}
s.executeTemplate(w, r, "deploy", data)
return data, true
}
func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug string) {
// appCommonData resolves an app by its slug and assembles everything EVERY tab of its page needs:
// the identity the shared shell renders (badges, address, banners), the "needs you" cards, the data
// folders and the migrate offer. It was appDetailHandler's whole body until v0.315.0, when the six
// separate app pages became one page with four tabs — the assembly did not change, only who calls it.
//
// It writes a 404 and reports false when no stack carries this slug, so a caller can `return` on it.
func (s *Server) appCommonData(w http.ResponseWriter, r *http.Request, slug string) (*stacks.Stack, map[string]interface{}, bool) {
var found *stacks.Stack
for _, stack := range s.stackMgr.GetStacks() {
if stack.Meta.Slug == slug {
@@ -748,7 +743,7 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
}
if found == nil {
http.NotFound(w, r)
return
return nil, nil, false
}
// Determine effective subdomain (stored env > metadata fallback)
@@ -810,7 +805,7 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
data["EffectiveSubdomain"] = effectiveSubdomain
// „Hova tegyem a fájlokat?" (R-75) — deployed apps that declare data_paths only. Set EXPLICITLY,
// like every other key here: appDetailHandler does not funnel through baseData, and the v0.150.0
// like every other key here: appCommonData does not funnel through baseData, and the v0.150.0
// app_export.html bug (a CSRF token rendered where the domain belonged) came from assuming it did.
if cards := s.buildDataPathCards(found, s.langFor(r)); len(cards) > 0 {
data["DataPathCards"] = cards
@@ -915,7 +910,7 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
data["HoldLogKept"] = s.stackMgr.HoldLogKept(found.Name)
}
s.executeTemplate(w, r, "app_info", data)
return found, data, true
}
func (s *Server) monitoringHandler(w http.ResponseWriter, r *http.Request) {
@@ -2656,7 +2651,7 @@ func (s *Server) appInitialCredsRevealHandler(w http.ResponseWriter, r *http.Req
escrowJSON(w, http.StatusServiceUnavailable, nil, s.msg(r, "escrow.stack_mgr_unavailable"))
return
}
// Resolved EXACTLY as appDetailHandler resolves it — same loop, same field. A second definition
// Resolved EXACTLY as appCommonData resolves it — same loop, same field. A second definition
// of "which app is this slug" is how a reveal ends up answering for a different app than the page
// the customer is looking at.
var found *stacks.Stack