v0.331.0: OnlyOffice opens and saves from FileBrowser and Nextcloud; FileBrowser back at once after a restart (R-1018)
gates / gates (push) Successful in 48s

FileBrowser config carries server.internalUrl http://filebrowser:80 (OnlyOffice fetched the public files address and got 403);
Nextcloud's DocumentServerInternalUrl gets its trailing slash (every save was lost: http://onlyoffice:80cache/...);
RestartStack("filebrowser") runs the R-1126 gate at once (files.<domain> was 502 for up to 5 min after the link switched).
Each red-proofed. MinAgent 0.158.1.
This commit is contained in:
2026-10-18 21:05:04 +02:00
parent 615940de35
commit 772ef36f5f
10 changed files with 317 additions and 44 deletions
+26
View File
@@ -1,3 +1,29 @@
## v0.331.0 — OnlyOffice opens and SAVES from FileBrowser and Nextcloud; FileBrowser is back at once after a restart (R-1018) (2026-10-18)
**MinAgent: 0.158.1** — the floor's, carried (not above the vouched agent 0.159.2). Nothing here calls a new agent route.
No golden (`09` §3 decision 250). Task: the evening brief of 2026-10-18 (felhom.eu `REPORT-evening-2026-10-18.md`).
Found by the first live walk of the OnlyOffice links since the move to Proxmox (scratch 9202, headless Chrome through the
box's public names; felhom.eu `audits/onlyoffice-2026-10-18/`).
- **FileBrowser → OnlyOffice could not open a document.** FileBrowser built the download and save URLs it hands
OnlyOffice from the request's host, `https://files.<domain>` — a trip out through the tunnel and back (on demo-hp it
reached another box: 403). FileBrowser's config now carries `server.internalUrl: "http://filebrowser:80"` (used by
integrations only, gtstef/filebrowser:1.5.6-stable `onlyOffice.go`); OnlyOffice reaches it by name on traefik-public,
where the R-1126 gate puts it. Every box's FileBrowser config changes once, so FileBrowser is re-created once.
`TestFileBrowserConfig_R1018_InternalURLIsTheContainer`.
- **Nextcloud → OnlyOffice lost every edit.** `DocumentServerInternalUrl` was `http://onlyoffice:80` with no slash, and
Nextcloud's connector appends paths: it fetched `http://onlyoffice:80cache/files/…` and logged „Track: … status 2
error", while the editor said „All changes saved". Now `http://onlyoffice:80/` (as `StorageUrl` already was). The occ
list is a function (`nextcloudApplyCommands`), so the test reads the values the apply really sends.
`TestNextcloudApply_R1018_InternalAddressesTakeAPath`. A box with the link already on gets the value at OnlyOffice's
or Nextcloud's next start (the link re-applies then).
- **files.<domain> answered 502 for up to 5 minutes after the link switched on or off** (its Apply and Revoke restart
FileBrowser, and a re-created FileBrowser starts off traefik-public — R-1126 — until the next base-stack round;
measured 4 min 8 s and 3 min 38 s). `RestartStack("filebrowser")` now runs the gate at once, in the background, like
the mounts sync. `TestRestartStack_R1018_FileBrowserPublishedAgainAtOnce`.
- Red-proofs: each fix removed → its test fails with the wrong value (felhom.eu `audits/onlyoffice-2026-10-18/redproof-fix{1,2,3}.txt`).
- No page changed; every fixture byte-identical.
## Unreleased (2026-10-18) — CI workflow: the failure mail links the public run page, by run id (R-1135); no code change
`.gitea/workflows/gates.yml`, the alarm step: the mail's run link is now `https://gitea.dooplex.hu/<repo>/actions/runs/<run id>`.
+2
View File
@@ -1,6 +1,8 @@
# CONTEXT.md — Project Memory
> **2026-10-18 (evening) — v0.331.0: OnlyOffice opens and saves from FileBrowser (`server.internalUrl`) and Nextcloud
> (`DocumentServerInternalUrl` with its slash); a FileBrowser restart runs the R-1126 gate at once (R-1018).** MinAgent 0.158.1.
> **2026-10-18 (day) — v0.330.0: „anyway" asks the agent's disk list first and starts again the apps it stopped when the
> agent refuses (R-1144).** MinAgent 0.158.1.
> **2026-10-18 (night) — v0.329.0: each app reaches only its own `appdata/<folder>` (R-1142, compiled-in folder map:
+18 -7
View File
@@ -1,10 +1,21 @@
# REPORT — controller v0.330.0 (2026-10-18, day)
# REPORT — controller v0.331.0 (2026-10-18 evening): the OnlyOffice links, live-walked and fixed (R-1018)
**In plain words.** When a household presses „retire anyway" and the host says no, the drive's apps start again, and
the household is told so. Where the host's own disk list already says no, nothing is stopped at all.
Task: the evening brief of 2026-10-18, Part 2 (felhom.eu `REPORT-evening-2026-10-18.md`). Evidence: felhom.eu
`documentation/audits/onlyoffice-2026-10-18/`.
| Row | Change | Pinned by | Red-proof |
|---|---|---|---|
| R-1144 | precheck from the agent's disk list; restart exactly the apps that were running after a refusal | `TestR1144_*` (3) | no restart → starts = []; no precheck → 3 stops before the refusal |
**The walk** (scratch 9202 on 0.330.0, OnlyOffice 9.4.0 + Nextcloud 34.0.4 installed through the dashboard API, headless
Chrome through the box's public names mapped to 9202):
Gates: green gate rc=0; `controller_gates.py --fast` OK. Release path and box walk: felhom.eu `REPORT-day-2026-10-18.md`.
| Path | Before the fix | After |
|---|---|---|
| FileBrowser → open a .docx | editor frame loads, the document never does: OnlyOffice's fetch of `https://files.<domain>/…` → 403 (the public name leads to another box) | opens, edits, saves: the file on the drive reads „R1018 BEFORE EDITED-FB-1849" |
| Nextcloud → open a .docx | opens and edits; „All changes saved" — but the save is lost: Nextcloud fetches `http://onlyoffice:80cache/…` | the file in Nextcloud reads „… EDITED-NC-1847" |
| switch the link on / off | files.<domain> 502 for 4 min 8 s / 3 min 38 s | the gate runs at the restart |
| OnlyOffice stopped | FileBrowser: no editor; Nextcloud: `/apps/onlyoffice/<id>` 404 — the revoke-on-stop rule holds | — |
**Fixes** (each red-proofed — the fix removed, the test failed with the wrong value, restored):
1. FileBrowser config: `server.internalUrl: "http://filebrowser:80"` — `TestFileBrowserConfig_R1018_InternalURLIsTheContainer`.
2. Nextcloud: `DocumentServerInternalUrl = http://onlyoffice:80/` — `TestNextcloudApply_R1018_InternalAddressesTakeAPath`.
3. `RestartStack("filebrowser")` runs the R-1126 gate — `TestRestartStack_R1018_FileBrowserPublishedAgainAtOnce`.
Gates: `go build/vet/test ./...` green; `controller_gates.py --fast` green. Fixtures: none changed.
+2 -2
View File
@@ -3893,7 +3893,7 @@ CRUD methods in settings.go: `GetIntegrationState`, `SetIntegrationState`, `Remo
#### Built-in Handlers
**OnlyOffice → FileBrowser** (`onlyoffice_filebrowser.go`):
- Apply: Reads `JWT_SECRET` + `SUBDOMAIN` from OnlyOffice app.yaml (decrypted), strips any existing `integrations:` block from FileBrowser `config.yaml` via `removeIntegrationsSection()`, appends new block with `url` (public HTTPS), `internalUrl` (`http://onlyoffice:80`), `secret`, `viewOnly: false`. Atomic write (`.tmp` + rename). Restarts FileBrowser
- Apply: Reads `JWT_SECRET` + `SUBDOMAIN` from OnlyOffice app.yaml (decrypted), strips any existing `integrations:` block from FileBrowser `config.yaml` via `removeIntegrationsSection()`, appends new block with `url` (public HTTPS), `internalUrl` (`http://onlyoffice:80`), `secret`, `viewOnly: false`. Atomic write (`.tmp` + rename). Restarts FileBrowser — and `RestartStack("filebrowser")` runs the R-1126 gate at once, so files.<domain> is back within seconds (v0.331.0, R-1018). OnlyOffice fetches and saves the document at FileBrowser's `server.internalUrl` (`http://filebrowser:80`, rendered in every FileBrowser config since v0.331.0) — never through the public address
- Revoke: Strips `integrations:` block from config.yaml, restarts FileBrowser
**OnlyOffice → Nextcloud** (`onlyoffice_nextcloud.go`):
@@ -3901,7 +3901,7 @@ CRUD methods in settings.go: `GetIntegrationState`, `SetIntegrationState`, `Remo
1. `app:install onlyoffice` (tolerates "already installed")
2. `app:enable onlyoffice`
3. `config:app:set onlyoffice DocumentServerUrl --value=https://{subdomain}.{domain}`
4. `config:app:set onlyoffice DocumentServerInternalUrl --value=http://onlyoffice:80`
4. `config:app:set onlyoffice DocumentServerInternalUrl --value=http://onlyoffice:80/` (the trailing slash is load-bearing: the connector appends paths — without it every save failed, v0.331.0, R-1018)
5. `config:app:set onlyoffice jwt_secret --value={JWT_SECRET}`
6. `config:app:set onlyoffice StorageUrl --value=http://nextcloud` (internal callback URL)
- Revoke: Runs `occ app:disable onlyoffice` (tolerates container not running / app not enabled)
+5
View File
@@ -459,6 +459,11 @@ func RenderFileBrowserConfigKept(paths []settings.StoragePath, importSource bool
server:
port: 80
baseURL: "/"
# R-1018: the address OnlyOffice's server uses to fetch and save a document (FileBrowser's onlyOffice.go builds both
# URLs from it at gtstef/filebrowser:1.5.6-stable; it is used by integrations only). Without it FileBrowser hands
# OnlyOffice https://files.<domain> — a trip out through the tunnel and back, which failed (403) on demo-hp.
# Pinned by TestFileBrowserConfig_R1018_InternalURLIsTheContainer.
internalUrl: "http://filebrowser:80"
# The box updates FileBrowser (the pinned image); a household cannot, so its „An update is available!" is noise.
# backend/common/settings/structs.go:51 at gtstef/filebrowser:1.5.6-stable (v0.321.0).
disableUpdateCheck: true
+29
View File
@@ -1,6 +1,8 @@
package infra
import (
"net/url"
"strconv"
"strings"
"testing"
@@ -248,6 +250,33 @@ func TestFileBrowserRender(t *testing.T) {
}
}
// R-1018: OnlyOffice's server fetches and saves a FileBrowser document at server.internalUrl. The consequence pinned:
// that URL names the FileBrowser container on port 80 — the name OnlyOffice resolves on traefik-public — and the
// container really is called that in the compose. RED-PROOF: drop the config line (FileBrowser then hands OnlyOffice
// the public https://files.<domain>, 403 on demo-hp 2026-10-18) or rename the container → this fails.
func TestFileBrowserConfig_R1018_InternalURLIsTheContainer(t *testing.T) {
var cfg struct {
Server struct {
InternalURL string `yaml:"internalUrl"`
Port int `yaml:"port"`
} `yaml:"server"`
}
if err := yaml.Unmarshal([]byte(RenderFileBrowserConfig([]settings.StoragePath{{Path: "/mnt/hdd_1"}}, true)), &cfg); err != nil {
t.Fatalf("config does not parse: %v", err)
}
u, err := url.Parse(cfg.Server.InternalURL)
if err != nil || u.Scheme != "http" || u.Hostname() == "" {
t.Fatalf("server.internalUrl = %q — OnlyOffice would be handed the public files address", cfg.Server.InternalURL)
}
if u.Port() != strconv.Itoa(cfg.Server.Port) {
t.Errorf("server.internalUrl port %q, FileBrowser listens on %d", u.Port(), cfg.Server.Port)
}
compose := RenderFileBrowserCompose("demo-felhom.eu", nil)
if !strings.Contains(compose, "container_name: "+u.Hostname()+"\n") {
t.Errorf("server.internalUrl names %q, which is not the FileBrowser container:\n%s", u.Hostname(), compose)
}
}
// R-1126 (D11 = A): FileBrowser starts OFF traefik-public — the box joins it there only after this run's default login
// is checked — and on its own network, which the controller joins for that check. v0.327.1: NO Traefik labels — the
// route is the file-provider route by NAME (a label route kept the address Traefik chose at container start, on the
@@ -29,41 +29,7 @@ func (h *OnlyOfficeNextcloudHandler) Apply(ac *ApplyContext) error {
}
publicURL := fmt.Sprintf("https://%s.%s", subdomain, ac.Domain)
internalURL := "http://onlyoffice:80"
// Install and configure OnlyOffice app in Nextcloud
commands := []struct {
args []string
tolerate string // substring in output to tolerate as success
}{
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "app:install", "onlyoffice"},
tolerate: "already installed",
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "app:enable", "onlyoffice"},
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "DocumentServerUrl", "--value=" + publicURL},
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "DocumentServerInternalUrl", "--value=" + internalURL},
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "jwt_secret", "--value=" + jwtSecret},
},
{
// StorageUrl: tells OO Document Server where to reach Nextcloud internally for file callbacks.
// Trailing slash is critical — NC's OO connector does string replacement of the server URL
// (which ends with /) with StorageUrl. Without trailing slash, "/apps/..." merges into hostname.
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "StorageUrl", "--value=http://nextcloud/"},
},
{
// Add internal hostname to trusted_domains so OO Document Server callbacks
// (arriving with Host: nextcloud) are not rejected by Nextcloud.
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:system:set", "trusted_domains", "10", "--value=nextcloud"},
},
}
commands := nextcloudApplyCommands(publicURL, jwtSecret)
for _, cmd := range commands {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
@@ -107,3 +73,52 @@ func (h *OnlyOfficeNextcloudHandler) Revoke(ac *ApplyContext) error {
ac.Logger.Printf("[INFO] [integrations] OnlyOffice integration revoked from Nextcloud")
return nil
}
// occCommand is one occ call of the apply; tolerate is a substring of its output that counts as success.
type occCommand struct {
args []string
tolerate string
}
// The two addresses OnlyOffice and Nextcloud use to reach each other inside the box. BOTH end with "/": Nextcloud's
// OnlyOffice connector appends paths to them („converter", „cache/files/…", „apps/onlyoffice/track"). R-1018,
// measured on demo-hp 2026-10-18: DocumentServerInternalUrl without the slash made Nextcloud fetch
// http://onlyoffice:80cache/files/… — every save failed („Track: … status 2 error") while the editor said
// „All changes saved". Pinned by TestNextcloudApply_R1018_InternalAddressesTakeAPath.
const (
ncDocServerInternalURL = "http://onlyoffice:80/"
ncStorageURL = "http://nextcloud/"
)
// nextcloudApplyCommands installs and configures the OnlyOffice app in Nextcloud.
func nextcloudApplyCommands(publicURL, jwtSecret string) []occCommand {
return []occCommand{
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "app:install", "onlyoffice"},
tolerate: "already installed",
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "app:enable", "onlyoffice"},
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "DocumentServerUrl", "--value=" + publicURL},
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "DocumentServerInternalUrl", "--value=" + ncDocServerInternalURL},
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "jwt_secret", "--value=" + jwtSecret},
},
{
// StorageUrl: tells OO Document Server where to reach Nextcloud internally for file callbacks.
// Trailing slash is critical — NC's OO connector does string replacement of the server URL
// (which ends with /) with StorageUrl. Without trailing slash, "/apps/..." merges into hostname.
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "StorageUrl", "--value=" + ncStorageURL},
},
{
// Add internal hostname to trusted_domains so OO Document Server callbacks
// (arriving with Host: nextcloud) are not rejected by Nextcloud.
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:system:set", "trusted_domains", "10", "--value=nextcloud"},
},
}
}
@@ -0,0 +1,44 @@
package integrations
import (
"net/url"
"strings"
"testing"
)
// R-1018: Nextcloud's OnlyOffice connector builds its server-to-server requests by APPENDING a path to the two inside
// addresses the apply sets. The consequence pinned: the address the apply really sends, plus such a path, is a request
// to the right container. Measured on demo-hp 2026-10-18: without the slash Nextcloud asked
// http://onlyoffice:80cache/files/… and every save was lost. RED-PROOF: drop the slash from ncDocServerInternalURL →
// the cache URL no longer parses (port "80cache") and this fails.
func TestNextcloudApply_R1018_InternalAddressesTakeAPath(t *testing.T) {
values := map[string]string{}
for _, c := range nextcloudApplyCommands("https://office.example.hu", "secret") {
if len(c.args) >= 11 && c.args[7] == "config:app:set" {
values[c.args[9]] = strings.TrimPrefix(c.args[10], "--value=")
}
}
for _, tc := range []struct {
key, path, host string
}{
{"DocumentServerInternalUrl", "cache/files/data/2153553391_5872/output.docx/output.docx", "onlyoffice:80"},
{"DocumentServerInternalUrl", "converter?shardkey=thumb_1", "onlyoffice:80"},
{"StorageUrl", "apps/onlyoffice/track?doc=x", "nextcloud"},
} {
base, ok := values[tc.key]
if !ok {
t.Fatalf("the apply does not set %s (set: %v)", tc.key, values)
}
u, err := url.Parse(base + tc.path)
if err != nil {
t.Errorf("%s %q + %q is not a URL: %v", tc.key, base, tc.path, err)
continue
}
if u.Host != tc.host || !strings.HasPrefix(u.Path, "/"+strings.SplitN(tc.path, "?", 2)[0][:4]) {
t.Errorf("%s %q + %q reaches host %q path %q, want host %q", tc.key, base, tc.path, u.Host, u.Path, tc.host)
}
}
if values["DocumentServerUrl"] != "https://office.example.hu" {
t.Errorf("DocumentServerUrl = %q, want the public office address", values["DocumentServerUrl"])
}
}
@@ -0,0 +1,132 @@
package stacks
import (
"context"
"fmt"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
)
// R-1018: the OnlyOffice link restarts FileBrowser (Apply and Revoke), and a re-created FileBrowser starts off
// traefik-public (R-1126). The consequence pinned: after RestartStack("filebrowser") the NEW run is checked and joined to
// traefik-public by itself, within seconds — not at the next base-stack round (files.<domain> answered 502 for 4 min 8 s
// on demo-hp, 2026-10-18). Another app's restart publishes nothing. RED-PROOF: remove the gate call from RestartStack →
// "not published again" (nothing connects until a round runs, and no round runs here).
func TestRestartStack_R1018_FileBrowserPublishedAgainAtOnce(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("the stub compose binary is a shell script")
}
fb := &fakeFB{password: "admin"}
srv := httptest.NewServer(fb.handler())
defer srv.Close()
m, _, _ := fbManager(t, srv.URL)
root := t.TempDir()
cfg := &config.Config{}
cfg.Paths.StacksDir = root
m.cfg = cfg
m.stacks = map[string]*Stack{}
for _, name := range []string{"filebrowser", "bookstack"} {
dir := filepath.Join(root, name)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "docker-compose.yml"), []byte("services:\n web:\n image: nginx:1.27\n"), 0o644); err != nil {
t.Fatal(err)
}
m.stacks[name] = &Stack{Name: name, ComposePath: filepath.Join(dir, "docker-compose.yml"), Deployed: true}
}
// compose: a stub that records the recreate; docker inspect: the start time moves after it (RestartStack's own check).
bin := t.TempDir()
marker := filepath.Join(t.TempDir(), "recreated")
script := fmt.Sprintf("#!/bin/sh\nfor a in \"$@\"; do [ \"$a\" = \"--force-recreate\" ] && touch %q; done\nexit 0\n", marker)
if err := os.WriteFile(filepath.Join(bin, "docker-compose"), []byte(script), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
m.composeCmd = "docker-compose"
m.execFn = func(string, ...string) (string, error) { return "", nil }
recreated := func() bool { _, err := os.Stat(marker); return err == nil }
m.installedExecFn = func(_ context.Context, _ string, _ []string, name string, args ...string) (string, error) {
if name == "docker" && len(args) > 0 && args[0] == "compose" {
name, args = "docker-compose", args[1:]
}
started := "2026-10-18T18:00:00Z"
if recreated() {
started = "2026-10-18T18:32:14Z"
}
switch {
case name == "docker-compose" && len(args) > 0 && args[0] == "ps":
return `{"ID":"aaa111bbb222","Name":"web-1","Service":"web"}`, nil
case name == "docker" && len(args) > 0 && args[0] == "inspect" && strings.Contains(strings.Join(args, " "), "State.StartedAt"):
return "aaa111bbb222ccc333" + inspectSep + "started" + inspectSep + started + "\n", nil
case name == "docker" && len(args) > 0 && args[0] == "inspect":
return "aaa111bbb222" + inspectSep + "nginx:1.27" + inspectSep + "sha256:i\n", nil
case name == "docker" && len(args) > 1 && args[0] == "image":
return "sha256:i" + inspectSep + "nginx@sha256:d\n", nil
}
return "", nil
}
// The gate's view of FileBrowser: run 1 was checked and public; a recreate makes run 2, off traefik-public.
var mu sync.Mutex
nets := map[string]bool{"filebrowser@traefik-public": true, "felhom-controller@felhom-filebrowser": true}
connected := make(chan string, 8)
m.fbStarted = func() string {
if recreated() {
return "run-2"
}
return "run-1"
}
m.fbProbedRun = "run-1"
m.fbNetOnFn = func(c, n string) bool { mu.Lock(); defer mu.Unlock(); return nets[c+"@"+n] }
m.fbNetConnectFn = func(c, n string) error {
mu.Lock()
nets[c+"@"+n] = true
mu.Unlock()
connected <- c + "@" + n
return nil
}
// Another app's restart publishes nothing.
if err := m.RestartStack("bookstack"); err != nil {
t.Fatalf("bookstack restart: %v", err)
}
select {
case c := <-connected:
t.Fatalf("a bookstack restart touched FileBrowser's networks: %s", c)
case <-time.After(300 * time.Millisecond):
}
os.Remove(marker)
// FileBrowser's restart: the recreate drops it from traefik-public; the gate must put run 2 back by itself.
mu.Lock()
delete(nets, "filebrowser@traefik-public")
mu.Unlock()
if err := m.RestartStack("filebrowser"); err != nil {
t.Fatalf("filebrowser restart: %v", err)
}
deadline := time.After(10 * time.Second)
for {
select {
case c := <-connected:
if c == "filebrowser@traefik-public" {
if m.fbProbedRun != "run-2" {
t.Fatalf("published, but the checked run is %q, not the new run-2", m.fbProbedRun)
}
return
}
case <-deadline:
t.Fatal("FileBrowser was not published again after its restart — files.<domain> stays 502 until a base-stack round")
}
}
}
+9
View File
@@ -1501,6 +1501,15 @@ func (m *Manager) RestartStack(name string) error {
}
m.mu.Unlock()
// R-1018: a re-created FileBrowser starts off traefik-public (R-1126), so files.<domain> answered 502 until the next
// base-stack round — up to 5 minutes, measured 4 min 8 s on demo-hp 2026-10-18 after the OnlyOffice link switched
// on (its Apply and Revoke restart FileBrowser). Check and publish this run now, in the background like the mounts
// sync does (handlers.go): the check may wait ~20 s for FileBrowser to answer, and it takes infraMu, which no caller
// of RestartStack holds. Pinned by TestRestartStack_R1018_FileBrowserPublishedAgainAtOnce.
if name == fileBrowserContainer {
go m.EnsureFileBrowserGate()
}
return m.RefreshStatus()
}