zdd-6 task 12: the instance page shows installed, serving and the hold, with Retry
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 files changed,
+290
−3
internal/console/browserretry.go+81 −0internal/console/browserretry_test.go+116 −0internal/console/claim_invariant_fixtures_test.go+24 −0internal/console/instancepage.go+54 −0internal/console/server.go+1 −0internal/console/templates/app_instance.html+10 −1internal/console/testdata/claim-routes.txt+1 −0internal/provision/plan9_b8_census_test.go+3 −2
diff --git a/internal/console/browserretry.go b/internal/console/browserretry.go| new file mode 100644 |
| index 000000000..b15053519 |
| --- /dev/null |
| +++ b/internal/console/browserretry.go |
| @@ -0,0 +1,81 @@ |
| +package console |
| + |
| +import ( |
| + "context" |
| + "crypto/subtle" |
| + "errors" |
| + "log" |
| + "net/http" |
| + "net/url" |
| + "strings" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/provision" |
| +) |
| + |
| +// handleInstanceRetry is the instance page's Retry (spec §9.2): the browser |
| +// twin of `carlos instances retry`, shaped like the restart action: session, |
| +// CSRF, the byte-identical 404, a confirmation page, then the write. It |
| +// reads and writes through the CLI door's helpers (retryHostBuild, |
| +// requestHostRetry), so the two cannot disagree about what is held. |
| +func (s *Server) handleInstanceRetry(w http.ResponseWriter, r *http.Request) { |
| + addr, raw, acct, role, ok := s.account(w, r) |
| + if !ok { |
| + return |
| + } |
| + if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 { |
| + s.renderMessage(w, r, http.StatusForbidden, "Refused", "Cross-site request blocked.") |
| + return |
| + } |
| + app := r.PathValue("app") |
| + host := strings.TrimSpace(r.PathValue("host")) |
| + has, err := s.store.HasAppIn(acct.ID, app) |
| + if err != nil { |
| + s.renderMessage(w, r, http.StatusInternalServerError, "Error", "Could not read your apps. Please try again.") |
| + return |
| + } |
| + if !has || !provision.ValidHostShape(host) || s.cfg.Releases == nil { |
| + http.NotFound(w, r) |
| + return |
| + } |
| + back := "/a/" + url.PathEscape(acct.Sqid) + "/apps/" + url.PathEscape(app) + "/instances/" + url.PathEscape(host) |
| + flash := func(kind, msg string) { |
| + s.setFlash(w, kind, msg) |
| + http.Redirect(w, r, back, http.StatusSeeOther) |
| + } |
| + ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) |
| + defer cancel() |
| + build, err := s.retryHostBuild(ctx, acct, app, host) |
| + switch { |
| + case errors.Is(err, errRetryNoRecord): |
| + http.NotFound(w, r) |
| + return |
| + case errors.Is(err, errRetryNotHeld): |
| + flash("err", strings.ReplaceAll(swapCopy.NotHeld, "{host}", host)) |
| + return |
| + case err != nil: |
| + log.Printf("console: browser retry %s/%s/%s: %v", acct.Sqid, app, host, err) |
| + flash("err", swapCopy.RetryFailed) |
| + return |
| + } |
| + fill := strings.NewReplacer("{host}", host, "{build}", build) |
| + if !confirmed(r) { |
| + s.renderConfirm(w, r, addr, raw, acct, role, app, &ConfirmView{ |
| + Action: r.URL.Path, Fields: [][2]string{{"csrf", raw}}, CancelURL: back, |
| + Title: fill.Replace(swapCopy.RetryTitle), Lines: []string{fill.Replace(swapCopy.RetryLine)}, |
| + Button: swapCopy.RetryButton, |
| + }) |
| + return |
| + } |
| + if !s.allowWrite(Principal{Kind: PrincipalUser, Address: addr}, time.Now()) { |
| + flash("err", swapCopy.RetryLimited) |
| + return |
| + } |
| + if _, err := s.requestHostRetry(ctx, acct.Sqid, app, host); err != nil { |
| + log.Printf("console: browser retry %s/%s/%s: %v", acct.Sqid, app, host, err) |
| + flash("err", swapCopy.RetryFailed) |
| + return |
| + } |
| + s.auditActivity(acct, app, addr, Activity{Action: "retry", TargetType: "instance", Target: host, Detail: fill.Replace(swapCopy.Audit)}) |
| + flash("ok", fill.Replace(swapCopy.RetryDone)) |
| +} |
diff --git a/internal/console/browserretry_test.go b/internal/console/browserretry_test.go| new file mode 100644 |
| index 000000000..fc6b61a00 |
| --- /dev/null |
| +++ b/internal/console/browserretry_test.go |
| @@ -0,0 +1,116 @@ |
| +package console |
| + |
| +import ( |
| + "context" |
| + "fmt" |
| + "net/http" |
| + "net/url" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/rolloutwords" |
| + "github.com/carlosframework/platform/internal/status" |
| +) |
| + |
| +const heldHost = "web.kass.carlosapp.dev" |
| + |
| +func heldFixture(t *testing.T) *consoleFixture { |
| + t.Helper() |
| + fx := newConsoleFixture(t) |
| + fx.addRoute(t, registry.Route{Host: heldHost, App: "kass", Account: fx.sqid, Kind: "instance", Status: "ready", Version: "v2", Channel: "stable"}) |
| + sibling(t, fx, "kass", heldHost, "stable") |
| + writeHeldBox(t, fx, heldHost, "kass", "v2") |
| + return fx |
| +} |
| + |
| +func TestInstancePageShowsInstalledServingAndTheHold(t *testing.T) { |
| + fx := heldFixture(t) |
| + c := signInPaul(t, fx) |
| + code, _, body := get(t, c, fx.u("/apps/kass/instances/"+heldHost)) |
| + if code != http.StatusOK { |
| + t.Fatalf("page = %d", code) |
| + } |
| + for _, want := range []string{ |
| + swapCopy.Installed, "<code>v2</code>", swapCopy.Serving, "<code>v1</code>", swapCopy.Hold, |
| + fmt.Sprintf(swapCopy.HoldText, "v2", rolloutwords.Reason(status.ReasonHealth), "v1"), |
| + fmt.Sprintf(swapCopy.PromoteRetry, "kass", "v2", "stable"), |
| + `action="/a/` + fx.sqid + `/apps/kass/instances/` + heldHost + `/retry"`, |
| + } { |
| + if !strings.Contains(body, want) { |
| + t.Errorf("page lacks %q", want) |
| + } |
| + } |
| +} |
| + |
| +func TestInstancePageWithoutAReportIsUnchanged(t *testing.T) { |
| + fx := newConsoleFixture(t) |
| + c := signInPaul(t, fx) |
| + _, _, body := get(t, c, fx.u("/apps/kass/instances/kass.carlosapp.dev")) |
| + if !strings.Contains(body, instanceCopy.Observed) || strings.Contains(body, swapCopy.Installed) { |
| + t.Fatal("a host with no rollout report must show the page as before") |
| + } |
| +} |
| + |
| +// A box that could not arm its scheduler reports only the installed build: |
| +// the page keeps Installed, shows no serving build, no hold and no Retry. |
| +func TestInstancePageOfAnUnavailableBoxShowsNoHoldAndNoRetry(t *testing.T) { |
| + fx := newConsoleFixture(t) |
| + fx.addRoute(t, registry.Route{Host: heldHost, App: "kass", Account: fx.sqid, Kind: "instance", Status: "ready", Version: "v2", Channel: "stable"}) |
| + sibling(t, fx, "kass", heldHost, "stable") |
| + box := status.CollectBox(nil, status.Probes{MachineID: "box1"}, time.Now()) |
| + box.Metrics = &status.BoxMetrics{ |
| + RouteVersions: map[string]string{heldHost: "v2"}, |
| + Rollout: &status.RolloutReport{Unavailable: "no-cgroup-delegation"}, |
| + } |
| + if err := status.WriteBox(context.Background(), fx.rel, box); err != nil { |
| + t.Fatal(err) |
| + } |
| + c := signInPaul(t, fx) |
| + code, _, body := get(t, c, fx.u("/apps/kass/instances/"+heldHost)) |
| + if code != http.StatusOK { |
| + t.Fatalf("page = %d", code) |
| + } |
| + if !strings.Contains(body, swapCopy.Installed) || !strings.Contains(body, "<code>v2</code>") { |
| + t.Error("an unavailable box's page must keep the installed build") |
| + } |
| + for _, bad := range []string{swapCopy.Hold, swapCopy.RetryButton, "/retry\"", "carlos promote --app"} { |
| + if strings.Contains(body, bad) { |
| + t.Errorf("an unavailable box's page must not show %q", bad) |
| + } |
| + } |
| +} |
| + |
| +func TestBrowserRetryConfirmsThenWrites(t *testing.T) { |
| + fx := heldFixture(t) |
| + c := signInPaul(t, fx) |
| + csrf := sessionToken(t, fx, c) |
| + endpoint := fx.u("/apps/kass/instances/" + heldHost + "/retry") |
| + code, _, body := postForm(t, c, endpoint, url.Values{"csrf": {csrf}}) |
| + title := strings.NewReplacer("{host}", heldHost, "{build}", "v2").Replace(swapCopy.RetryTitle) |
| + if code != http.StatusOK || !strings.Contains(body, title) { |
| + t.Fatalf("confirm = %d, lacks %q", code, title) |
| + } |
| + code, loc, _ := postGated(t, fx, c, endpoint, url.Values{"csrf": {csrf}, "confirmed": {"1"}}) |
| + if code != http.StatusSeeOther || loc != fx.path("/apps/kass/instances/"+heldHost) { |
| + t.Fatalf("retry = %d -> %q", code, loc) |
| + } |
| + if r := lastScoped(t, fx, "kass"); r.Scope.Kind != release.RestartScopeRetry || r.Scope.Value != heldHost { |
| + t.Fatalf("stored = %+v", r) |
| + } |
| +} |
| + |
| +func TestBrowserRetryRefusals(t *testing.T) { |
| + fx := heldFixture(t) |
| + c := signInPaul(t, fx) |
| + if code, _, _ := postForm(t, c, fx.u("/apps/kass/instances/"+heldHost+"/retry"), url.Values{"csrf": {"wrong"}}); code != http.StatusForbidden { |
| + t.Fatalf("bad csrf = %d", code) |
| + } |
| + csrf := sessionToken(t, fx, c) |
| + code, _, body := postForm(t, c, fx.u("/apps/kass/instances/nope.kass.carlosapp.dev/retry"), url.Values{"csrf": {csrf}, "confirmed": {"1"}}) |
| + if code != http.StatusNotFound || body != "404 page not found\n" { |
| + t.Fatalf("unknown host = %d %q", code, body) |
| + } |
| +} |
diff --git a/internal/console/claim_invariant_fixtures_test.go b/internal/console/claim_invariant_fixtures_test.go| index ec5ef687e..50c8df0ad 100644 |
| --- a/internal/console/claim_invariant_fixtures_test.go |
| +++ b/internal/console/claim_invariant_fixtures_test.go |
| @@ -911,6 +911,30 @@ var claimFixtures = map[string]claimFixture{ |
| apps: claimRefs("bab/kass"), |
| do: claimForm("/a/bab/apps/kass/instances/kass.carlosapp.dev/restart", "confirmed", "1"), |
| }}, |
| + "route POST /a/{sqid}/apps/{app}/instances/{host}/retry": {door: &claimDoor{ |
| + apps: claimRefs("bab/titogo"), |
| + setup: func(t *testing.T, cx *claimCase) { cx.heldInstance(t, "web.go.tito.io") }, |
| + do: claimForm("/a/bab/apps/titogo/instances/web.go.tito.io/retry", "confirmed", "1"), |
| + check: func(t *testing.T, cx *claimCase, open bool) string { |
| + g, found, err := release.LoadRestartScopes(context.Background(), cx.rel, "bab", "titogo") |
| + if err != nil { |
| + return err.Error() |
| + } |
| + asked := false |
| + if found { |
| + for _, r := range g.Requests { |
| + asked = asked || (r.Scope.Kind == release.RestartScopeRetry && r.Scope.Value == "web.go.tito.io") |
| + } |
| + } |
| + switch { |
| + case open && !asked: |
| + return "the retry request was not written: the handler never reached the narrowed log" |
| + case !open && asked: |
| + return "a retry request was written while the app was closed under a foreign exclusive operation" |
| + } |
| + return "" |
| + }, |
| + }}, |
| "route POST /a/{sqid}/apps/{app}/keep": {door: &claimDoor{ |
| apps: claimRefs("bab/tmp-claim-keep"), |
| setup: func(t *testing.T, cx *claimCase) { cx.temporary(t, "tmp-claim-keep") }, |
diff --git a/internal/console/instancepage.go b/internal/console/instancepage.go| index 7d3236754..412f7473d 100644 |
| --- a/internal/console/instancepage.go |
| +++ b/internal/console/instancepage.go |
| @@ -3,11 +3,14 @@ package console |
| import ( |
| "encoding/base64" |
| "encoding/json" |
| + "fmt" |
| "net/http" |
| "net/url" |
| "sort" |
| + "time" |
| "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/rolloutwords" |
| "github.com/carlosframework/platform/internal/status" |
| ) |
| @@ -21,6 +24,53 @@ type instanceDetail struct { |
| // reason, which can carry a path (R10). The approved fallback text |
| // when no box's report names this host. |
| Layout string |
| + // Rollout is this host's rollout as its box last reported it (spec |
| + // §9.2); nil when no fresh box reports one, and the page is as before. |
| + // App and Channel name the `promote --retry` the hold suggests. |
| + Rollout *rolloutView |
| + App, Channel string |
| +} |
| + |
| +// HoldText is the Failure hold row (spec §9.2), or "" when nothing holds |
| +// this host back (and always for a box that could not arm its scheduler, |
| +// whose view carries no hold). |
| +func (d *instanceDetail) HoldText() string { |
| + r := d.Rollout |
| + if r == nil || r.Unavailable != "" || r.HeldBy == "" { |
| + return "" |
| + } |
| + stays := r.Serving |
| + if stays == "" { |
| + stays = r.Good |
| + } |
| + return fmt.Sprintf(swapCopy.HoldText, r.HeldBy, rolloutwords.Reason(r.Reason), stays) |
| +} |
| + |
| +// PromoteRetryText names the app-wide retry (spec rev 14 §8.4) beside the |
| +// one-instance Retry button. |
| +func (d *instanceDetail) PromoteRetryText() string { |
| + if d.HoldText() == "" { |
| + return "" |
| + } |
| + return fmt.Sprintf(swapCopy.PromoteRetry, d.App, d.Rollout.HeldBy, d.Channel) |
| +} |
| + |
| +// RolloutNote explains a serving build that differs from the installed one |
| +// when no failure holds it: rolling out, or asleep until the next request. |
| +// Plan 5 reports Serving "" for an asleep instance, so asleep compares the |
| +// installed build with Good, the build it last ran. A box that could not arm |
| +// its scheduler reports neither, so it gets no note. |
| +func (d *instanceDetail) RolloutNote() string { |
| + r := d.Rollout |
| + switch { |
| + case r == nil || r.Unavailable != "" || r.HeldBy != "": |
| + return "" |
| + case !r.Awake && r.Installed != r.Good: |
| + return fmt.Sprintf(swapCopy.Asleep, r.Installed) |
| + case r.Awake && r.Installed != r.Serving: |
| + return fmt.Sprintf(swapCopy.Rolling, r.Installed, r.Serving) |
| + } |
| + return "" |
| } |
| // layoutUnknownCopy is console.instance.layout.unknown (approved copy): |
| @@ -178,10 +228,14 @@ func (s *Server) handleAppInstance(w http.ResponseWriter, r *http.Request) { |
| // helper the console's other box readers use) — nil Releases or a read |
| // error both leave the approved unknown text already set above, the |
| // same soft-degrade the alias lookup just below gives a broken registry. |
| + detail.App, detail.Channel = p.AppName, selected.Channel |
| if s.cfg.Releases != nil { |
| if boxes, _, err := s.systemBoxes(r.Context(), false); err == nil { |
| detail.Layout = layoutWordFor(boxes, host) |
| } |
| + // The rollout view comes from the same platform-box join the retry |
| + // door reads (loadBoxJoin), not from systemBoxes. |
| + detail.Rollout = loadBoxJoin(r.Context(), s.cfg.Releases, time.Now()).rollout(host, acct.Sqid, p.AppName) |
| } |
| if reg, err := openRegistry(s.cfg.Registry); err == nil { |
| if aliases, err := reg.ListAliases(); err == nil { |
diff --git a/internal/console/server.go b/internal/console/server.go| index d9221584b..9decf7ebe 100644 |
| --- a/internal/console/server.go |
| +++ b/internal/console/server.go |
| @@ -1347,6 +1347,7 @@ func NewServer(cfg Config, st *Store) (*Server, error) { |
| s.registerRoute("GET /a/{sqid}/apps/{app}/instances/{host}", s.handleAppInstance) |
| s.registerRoute("POST /a/{sqid}/apps/{app}/restart", s.handleAppRestart) |
| s.registerRoute("POST /a/{sqid}/apps/{app}/instances/{host}/restart", s.handleInstanceRestart) |
| + s.registerRoute("POST /a/{sqid}/apps/{app}/instances/{host}/retry", s.handleInstanceRetry) |
| s.registerRoute("GET /a/{sqid}/apps/{app}/logs", s.handleAppLogs) |
| s.registerRoute("GET /a/{sqid}/apps/{app}/logs/data", s.handleAppLogsData) |
| // The Errors tab and its two setting doors (errorsview.go): the tab is |
diff --git a/internal/console/templates/app_instance.html b/internal/console/templates/app_instance.html| index e5c4b97dc..bb68c62bb 100644 |
| --- a/internal/console/templates/app_instance.html |
| +++ b/internal/console/templates/app_instance.html |
| @@ -15,12 +15,21 @@ |
| <dt>{{$.InstanceCopy.LastReading}}</dt><dd>{{if .Row.Metrics.ObservedAt.IsZero}}—{{else}}<time title="{{fmtTime .Row.Metrics.ObservedAt}}">{{ago .Row.Metrics.ObservedAt}}</time>{{end}}</dd> |
| <dt>Channel</dt><dd><code>{{.Row.Channel}}</code></dd> |
| <dt>Layout</dt><dd>{{.Layout}}</dd> |
| - <dt>{{$.InstanceCopy.Observed}}</dt><dd>{{if .Row.Version}}<code>{{.Row.Version}}</code>{{else}}—{{end}}</dd> |
| + {{if .Rollout}}<dt>{{$.SwapCopy.Installed}}</dt><dd><code>{{.Rollout.Installed}}</code></dd> |
| + <dt>{{$.SwapCopy.Serving}}</dt><dd>{{if .Rollout.Serving}}<code>{{.Rollout.Serving}}</code>{{else}}—{{end}}</dd> |
| + {{if .HoldText}}<dt>{{$.SwapCopy.Hold}}</dt><dd>{{.HoldText}}</dd>{{end}} |
| + {{else}}<dt>{{$.InstanceCopy.Observed}}</dt><dd>{{if .Row.Version}}<code>{{.Row.Version}}</code>{{else}}—{{end}}</dd>{{end}} |
| <dt>{{$.InstanceCopy.Target}}</dt><dd>{{if .TargetErr}}Can't tell right now{{else if .TargetVersion}}<code>{{.TargetVersion}}</code>{{else}}—{{end}}</dd> |
| <dt>CPU</dt><dd>{{pct .Row.Metrics.CPUFrac}}{{if .Row.Metrics.SparkCPU}}<svg class="cellspark" aria-hidden="true" viewBox="0 0 240 40" preserveAspectRatio="none"><polyline fill="none" stroke="currentColor" points="{{.Row.Metrics.SparkCPU}}"/></svg>{{end}}</dd> |
| <dt>Memory</dt><dd>{{bytesOrDash .Row.Metrics.MemBytes}}{{if .Row.Metrics.SparkMem}}<svg class="cellspark" aria-hidden="true" viewBox="0 0 240 40" preserveAspectRatio="none"><polyline fill="none" stroke="currentColor" points="{{.Row.Metrics.SparkMem}}"/></svg>{{end}}</dd> |
| {{if .Aliases}}<dt>Web addresses</dt><dd>{{range .Aliases}}<div><a href="https://{{.}}">{{.}}</a></div>{{end}}</dd>{{end}} |
| </dl> |
| +{{with .PromoteRetryText}}<p class="small">{{.}}</p>{{end}} |
| +{{if .HoldText}}<form method="post" action="{{$.Base}}/apps/{{$.AppName}}/instances/{{.Row.Host}}/retry"> |
| + <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
| + <button type="submit" class="btn-sm">{{$.SwapCopy.RetryButton}}</button> |
| +</form>{{end}} |
| +{{with .RolloutNote}}<p class="small">{{.}}</p>{{end}} |
| {{if eq .Row.HealthWord "No readings"}}<p class="small">{{$.InstanceCopy.NoReadings}}</p>{{end}} |
| {{if eq .Row.HealthWord "Asleep"}}<p class="small">wakes on the next request</p>{{end}} |
| {{if and .Row.Version .TargetVersion (not .TargetErr) (ne .Row.Version .TargetVersion)}}<p class="small">{{$.InstanceCopy.PendingVersion}}</p>{{end}} |
diff --git a/internal/console/testdata/claim-routes.txt b/internal/console/testdata/claim-routes.txt| index f05179dc1..b54c2dc79 100644 |
| --- a/internal/console/testdata/claim-routes.txt |
| +++ b/internal/console/testdata/claim-routes.txt |
| @@ -120,6 +120,7 @@ route POST /a/{sqid}/apps/{app}/errors/level |
| route POST /a/{sqid}/apps/{app}/fasttrack |
| route POST /a/{sqid}/apps/{app}/gate |
| route POST /a/{sqid}/apps/{app}/instances/{host}/restart |
| +route POST /a/{sqid}/apps/{app}/instances/{host}/retry |
| route POST /a/{sqid}/apps/{app}/keep |
| route POST /a/{sqid}/apps/{app}/move |
| route POST /a/{sqid}/apps/{app}/pending/{id}/confirm |
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go| index 38319e017..05e671140 100644 |
| --- a/internal/provision/plan9_b8_census_test.go |
| +++ b/internal/provision/plan9_b8_census_test.go |
| @@ -573,6 +573,7 @@ var plan9B8CensusInventory = []censusEntry{ |
| // Registers instance and sidecar HTTP paths; record handlers use the reviewed provision writers. |
| {"internal/console/authcli.go", "Server.registerCLIRoutes", "key-site", "lit:instances,lit:sidecars", "eb6388aed42540177e6b1531fb6e79ed4d66127ec8774c8fb68858ffd0c79296"}, |
| {"internal/console/browserrestart.go", "Server.handleBrowserRestart", "key-site", "lit:instances", "955d71813504f2cfec1e1f64817d5aa5a921aa38e1044c0e903d2cfbe4d073f4"}, |
| + {"internal/console/browserretry.go", "Server.handleInstanceRetry", "key-site", "lit:instances", "3da1e4c06eb91d1101461e78efaa58c39cf0d7d0ec9810317084fe51b0774a0d"}, |
| {"internal/console/channelmove.go", "Server.handleAPIInstanceChannel", "record-writer", "key:UpdateRecord", "91a2b3157bde9af832e8d95b17d0c384910229982945818f8b52f62dafddb342"}, |
| {"internal/console/channelmove.go", "Server.executePendingRouteChannel", "record-writer", "key:UpdateRecord", "b3b9a12213261a70b680ad5290957ebe7434eff633a73c2d80c75cc24d026d9a"}, |
| {"internal/console/claim_alias.go", "claimAliasStore.Put", "nonrecord", "def:Put,sel:Put", "a21b7cce328ebad9955211657ca8353adc7c2495b065fe09909b93c85ea7defb"}, |
| @@ -593,11 +594,11 @@ var plan9B8CensusInventory = []censusEntry{ |
| {"internal/console/geoippublish.go", "geoipPublisher.ExpireIfDue", "nonrecord", "sel:Put", "a7d4f24a25c7bda40f18268d34a99203b58aa07bc10eda0db2306b54cb08302b"}, |
| {"internal/console/hostreports.go", "putHostReport", "nonrecord", "sel:Put", "d8ba8c77b5f0ee5bcde5c11f4a608cf2bd2491c36b8cb9bf26753a230d485ad4"}, |
| {"internal/console/infrapass.go", "Server.putInventory", "nonrecord", "sel:Put", "c5d930ec178ee206d25194a2b265738ac42c6f00bd9b73626e4d46d769d370ad"}, |
| - {"internal/console/instancepage.go", "Server.handleAppInstance", "key-site", "lit:instances", "51073a3eb95b98982de74259fc99dd0aa375c8c1504213c7dd5ef58682daec8e"}, |
| + {"internal/console/instancepage.go", "Server.handleAppInstance", "key-site", "lit:instances", "95147b261febf9a40976bef10c6a55b8c328ef0412745d991b3372e2a7d3be10"}, |
| {"internal/console/relayops.go", "Server.relayAcquire", "nonrecord", "sel:Put", "3c757e1c92f1c0ce41c3b5f00aa1397ce5a0c4faabc2f176b23355cf5a41f5fb"}, |
| {"internal/console/relayops.go", "Server.relayPublishOnce", "nonrecord", "sel:Put", "783a3c20337914e08f92be783f6b48ab14288437a8043c0d5bf29f281a7a9b1d"}, |
| {"internal/console/relayops.go", "Server.relayRotate", "nonrecord", "sel:Put", "58e0090a4990e2d5e793cec2ec7bcbfb3626b4043307f64484a55766099e1434"}, |
| - {"internal/console/server.go", "NewServer", "key-site", "lit:instances", "78de5c1959d047aa5025fc51a7e37a568d29257384dc022a0ea020448fa6dd9e"}, |
| + {"internal/console/server.go", "NewServer", "key-site", "lit:instances", "87f202afe1e346f072109f6b19cc3ce8bc68b7a392ad1c9cdb329951e5ab1cfd"}, |
| {"internal/console/server.go", "Server.PublishMovesConsole", "nonrecord", "sel:PutReplace", "b228397d413769f93209645db9ac6bd4a47cc9db31f06c8699cb540cffea9b5a"}, |
| {"internal/console/server.go", "Server.handleAppInstances", "key-site", "lit:instances", "7368e00fa1bf20365b61361815db216441b7fd21b3d330349dc09170045d45b7"}, |
| {"internal/console/server.go", "Server.handleAppActivity", "key-site", "lit:instances", "265425be83a63bf2fbcd88e1b75600933cf1a2e8b7fa9cc90046fdd329db9786"}, |