You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/09/12 12:59:04 UTC

(juneau) branch release-manager updated: WORK-R0007: HTML-slot Admin pair pages ViewSlot.envelope + regions.mount, 302 /rest/admin to /releases, shared /rest/releases/view, keep secret-store Credentials, CSRF on shell.

This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch release-manager
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/release-manager by this push:
     new 8ddc8e4937 WORK-R0007: HTML-slot Admin pair pages ViewSlot.envelope + regions.mount, 302 /rest/admin to /releases, shared /rest/releases/view, keep secret-store Credentials, CSRF on shell.
8ddc8e4937 is described below

commit 8ddc8e49372e61bdf0eab3429649359a33dae96c
Author: James Bognar <ja...@apache.org>
AuthorDate: Sat Sep 12 08:58:58 2026 -0400

    WORK-R0007: HTML-slot Admin pair pages ViewSlot.envelope + regions.mount, 302 /rest/admin to /releases, shared /rest/releases/view, keep secret-store Credentials, CSRF on shell.
---
 .../org/apache/juneau/releng/rest/AdminRest.java   |  76 ++++------
 .../org/apache/juneau/releng/rest/ConsolePage.java |   5 +-
 .../apache/juneau/releng/rest/CredentialRest.java  |  17 ++-
 .../org/apache/juneau/releng/rest/ReleaseRest.java |  42 +++---
 .../apache/juneau/releng/rest/TableSlotPage.java   |  73 ++++++++++
 src/main/resources/static/js/table-slot.js         |  54 +++++++
 src/main/resources/templates/admin.ftlh            |  20 +--
 src/main/resources/templates/base.ftlh             |  25 ++--
 src/main/resources/templates/releases.ftlh         |  12 +-
 .../apache/juneau/releng/rest/AdminRestTest.java   | 162 ++++++++++++---------
 .../juneau/releng/rest/CredentialRestTest.java     |  81 +++++++++++
 .../apache/juneau/releng/rest/ReleaseRestTest.java |  38 +++++
 12 files changed, 426 insertions(+), 179 deletions(-)

diff --git a/src/main/java/org/apache/juneau/releng/rest/AdminRest.java b/src/main/java/org/apache/juneau/releng/rest/AdminRest.java
index 354052167d..842521e8bf 100644
--- a/src/main/java/org/apache/juneau/releng/rest/AdminRest.java
+++ b/src/main/java/org/apache/juneau/releng/rest/AdminRest.java
@@ -17,8 +17,10 @@
 
 package org.apache.juneau.releng.rest;
 
+import static org.apache.juneau.http.HttpResponses.*;
+
 import org.apache.juneau.commons.inject.Bean;
-import org.apache.juneau.marshall.html.HtmlSerializer;
+import org.apache.juneau.http.response.Found;
 import org.apache.juneau.rest.server.Rest;
 import org.apache.juneau.rest.server.RestGet;
 import org.apache.juneau.rest.server.RestRequest;
@@ -27,29 +29,24 @@ import org.apache.juneau.rest.server.view.View;
 import org.apache.juneau.rest.server.view.freemarker.FreemarkerMixin;
 import org.apache.juneau.rest.server.view.freemarker.FreemarkerViewRenderer;
 import org.apache.juneau.rest.server.view.freemarker.console.ConsoleFreemarkerMixin;
-import org.apache.juneau.rest.server.views.PageDef;
-import org.apache.juneau.rest.server.views.PageTable;
-import org.apache.juneau.rest.server.views.Tab;
 import org.apache.juneau.rest.server.views.ViewsMixin;
 
 /**
- * Admin tab: a single multi-tab page composing the app's existing
- * {@link ReleaseRest#releasesView() Releases} and {@link CredentialRest#credentialsView() Credentials} rich views
- * into one {@link PageDef}, rendered by {@link PageTable}.
+ * Admin tab: author-HTML pair pages that mount the app's existing
+ * {@link ReleaseRest#releasesView() Releases} and {@link CredentialRest#credentialsView() Credentials} tables
+ * into empty slots via {@code JuneauViews.regions.mount}.
  *
  * <p>
- * This resource references each tab's child {@link org.apache.juneau.rest.server.views.ViewDef ViewDef} by calling
- * the sibling resources' declarative view-builder methods directly &mdash; it does not duplicate their column/ribbon
- * definitions, and each child view's {@code dataUrl} stays absolute (its owning resource's own mount), so the
- * ajax data draws still hit {@link ReleaseRest#data()} / {@link CredentialRest#status()} exactly as they do from
- * the standalone Releases/Credentials pages. Per {@link PageTable}'s contract, the emitted per-view markup (marker
- * table + VIEW_META sidecar) is byte-for-byte identical to what {@link ReleaseRest#page(RestRequest)} /
- * {@code CredentialRest}'s own view would emit standalone &mdash; this resource only adds the tab-bar/panel shell
- * and the PAGE_META sidecar around them.
+ * Each pair is a full page load. {@code GET /} 302s to the Releases pair. Tables are fetched as
+ * {@code ViewSlot} JSON from the owning resources; this resource does not emit {@code ViewTable} markup
+ * or load {@code juneau-pages.js}.
  */
 @Rest(path = "/admin", title = "Admin", responseProcessors = FreemarkerViewRenderer.class, mixins = ViewsMixin.class)
 public class AdminRest extends BasicRestResource {
 
+	static final String RELEASES_URL = "/rest/admin/releases";
+	static final String CREDENTIALS_URL = "/rest/admin/credentials";
+
 	// Return type stays FreemarkerMixin - FreemarkerViewRenderer does an exact-type bean lookup (see
 	// ConsoleFreemarkerMixin's class Javadoc).
 	@Bean
@@ -57,42 +54,25 @@ public class AdminRest extends BasicRestResource {
 		return ConsoleFreemarkerMixin.create().basePath("/templates/").templateSuffix(".ftlh").build();
 	}
 
-	/**
-	 * The composed page definition: one leaf tab per existing rich view. {@code build()} validates unique tab ids
-	 * and unique referenced {@code ViewDef} ids across the page.
-	 */
-	static PageDef adminPage() {
-		return PageDef.create("admin")
-			.title("Admin")
-			.tabs(
-				Tab.create("releases", "Releases").view(ReleaseRest.releasesView()),
-				Tab.create("credentials", "Credentials").view(CredentialRest.credentialsView()))
-			.build();
+	/** Default Admin URL — 302 to the Releases pair. */
+	@RestGet("/")
+	public Found redirectToReleases() {
+		return found(RELEASES_URL);
+	}
+
+	/** Admin / Releases pair: empty {@code #releases} slot mounted from {@code /rest/releases/view}. */
+	@RestGet("/releases")
+	public View releases(RestRequest req) {
+		return pairPage(req, "releases", ReleaseRest.MOUNT + "/view", "releases");
 	}
 
-	/** Human page &mdash; the composed tab/sub-tab page shell (emitted as trusted markup) + PAGE_META sidecar. */
-	@RestGet("/")
-	public View page(RestRequest req) {
-		var markup = HtmlSerializer.DEFAULT_SIMPLE_SQ.toString(PageTable.of(req, adminPage()));
-		return ConsolePage.of("admin", req)
-			.attr("pageTable", markup)
-			.attr("viewsCssUrl", asset(req, ViewsMixin.VIEWS_CSS_PATH))
-			.attr("configCssUrl", asset(req, ViewsMixin.CONFIG_CSS_PATH))
-			.attr("rendersJsUrl", asset(req, ViewsMixin.RENDERS_JS_PATH))
-			.attr("iconsJsUrl", asset(req, ViewsMixin.ICONS_JS_PATH))
-			.attr("ribbonJsUrl", asset(req, ViewsMixin.RIBBON_JS_PATH))
-			.attr("viewsJsUrl", asset(req, ViewsMixin.VIEWS_JS_PATH))
-			.attr("configJsUrl", asset(req, ViewsMixin.CONFIG_JS_PATH))
-			.attr("pagesJsUrl", asset(req, ViewsMixin.PAGES_JS_PATH));
+	/** Admin / Credentials pair: empty {@code #credentials} slot mounted from {@code /rest/credentials/view}. */
+	@RestGet("/credentials")
+	public View credentials(RestRequest req) {
+		return pairPage(req, "credentials", CredentialRest.MOUNT + "/view", "credentials");
 	}
 
-	/**
-	 * Resolves a toolkit asset to an absolute, cache-busted URL for the FreeMarker head block via the
-	 * request-aware {@link ViewsMixin#viewAssetUrl(RestRequest, String)} &mdash; resolved per-request
-	 * against this resource's actual mount/context path, rather than a hardcoded {@code MOUNT} constant string-
-	 * replace, so moving this resource no longer silently breaks asset loading.
-	 */
-	private static String asset(RestRequest req, String path) {
-		return ViewsMixin.viewAssetUrl(req, path);
+	private static View pairPage(RestRequest req, String slotId, String tableUrl, String selectedChild) {
+		return TableSlotPage.of("admin", req, slotId, tableUrl).attr("selectedChild", selectedChild);
 	}
 }
diff --git a/src/main/java/org/apache/juneau/releng/rest/ConsolePage.java b/src/main/java/org/apache/juneau/releng/rest/ConsolePage.java
index ddf708fec3..3d1c4e28c2 100644
--- a/src/main/java/org/apache/juneau/releng/rest/ConsolePage.java
+++ b/src/main/java/org/apache/juneau/releng/rest/ConsolePage.java
@@ -17,6 +17,7 @@
 
 package org.apache.juneau.releng.rest;
 
+import org.apache.juneau.rest.server.filter.LoopbackBoundary;
 import org.apache.juneau.rest.server.filter.LoopbackBoundaryFilter;
 import org.apache.juneau.rest.server.view.freemarker.FreemarkerView;
 
@@ -55,6 +56,8 @@ final class ConsolePage {
 	 */
 	static FreemarkerView of(String template, HttpServletRequest req) {
 		var token = req.getAttribute(LoopbackBoundaryFilter.TOKEN_ATTRIBUTE);
-		return FreemarkerView.of(template).attr("csrfToken", token == null ? "" : token);
+		return FreemarkerView.of(template)
+			.attr("csrfToken", token == null ? "" : token)
+			.attr("csrfHeader", LoopbackBoundary.DEFAULT_CSRF_HEADER);
 	}
 }
diff --git a/src/main/java/org/apache/juneau/releng/rest/CredentialRest.java b/src/main/java/org/apache/juneau/releng/rest/CredentialRest.java
index fa7e14beeb..fa6abe46e0 100644
--- a/src/main/java/org/apache/juneau/releng/rest/CredentialRest.java
+++ b/src/main/java/org/apache/juneau/releng/rest/CredentialRest.java
@@ -21,11 +21,13 @@ import java.util.List;
 import org.apache.juneau.commons.inject.Bean;
 import org.apache.juneau.http.Content;
 import org.apache.juneau.http.Path;
+import org.apache.juneau.marshall.json.JsonSerializer;
 import org.apache.juneau.rest.server.Mutating;
 import org.apache.juneau.rest.server.Rest;
 import org.apache.juneau.rest.server.RestDelete;
 import org.apache.juneau.rest.server.RestGet;
 import org.apache.juneau.rest.server.RestPost;
+import org.apache.juneau.rest.server.RestRequest;
 import org.apache.juneau.rest.server.servlet.BasicRestResource;
 import org.apache.juneau.rest.server.view.View;
 import org.apache.juneau.rest.server.view.freemarker.FreemarkerMixin;
@@ -36,6 +38,7 @@ import org.apache.juneau.rest.server.views.RibbonAction;
 import org.apache.juneau.rest.server.views.ViewDef;
 import org.apache.juneau.rest.server.views.ViewDef.DataMode;
 import org.apache.juneau.rest.server.views.ViewDef.Dir;
+import org.apache.juneau.rest.server.views.ViewSlot;
 import org.apache.juneau.releng.credential.CredentialService;
 import org.apache.juneau.releng.credential.CredentialStatus;
 import org.apache.juneau.releng.credential.Validator.ValidationResult;
@@ -69,7 +72,7 @@ public class CredentialRest extends BasicRestResource {
 
 	/**
 	 * The rich-view toolkit's declarative view of the Credentials list: a second, independently-composable
-	 * {@link ViewDef} alongside {@link ReleaseRest#releasesView()}, wired into the RM {@code Admin} tab page
+	 * {@link ViewDef} alongside {@link ReleaseRest#releasesView()}, mounted into the Admin Credentials pair
 	 * ({@code AdminRest}). Client-side data mode: {@link #status()} already returns the bare
 	 * {@code List<CredentialStatus>} the toolkit's client-mode ajax (({@code dataSrc: ""})) expects, so no new
 	 * server-side query wiring is needed.
@@ -103,6 +106,18 @@ public class CredentialRest extends BasicRestResource {
 		return ConsolePage.of(NAME, req).attr(NAME, service.status());
 	}
 
+	/**
+	 * Slot envelope for the Admin Credentials table. JSON on this resource; the secret-store page does not
+	 * compose {@code ViewsMixin}.
+	 *
+	 * @param req The current request. Must not be {@code null}.
+	 * @return SLOT_META wrapping {@link #credentialsView()}.
+	 */
+	@RestGet(path = "/view", produces = "application/json", serializers = JsonSerializer.class)
+	public ViewSlot credentialsViewEnvelope(RestRequest req) {
+		return ViewSlot.envelope(req, credentialsView());
+	}
+
 	/** JSON status for all credentials (no secrets). */
 	@RestGet("/status")
 	public List<CredentialStatus> status() {
diff --git a/src/main/java/org/apache/juneau/releng/rest/ReleaseRest.java b/src/main/java/org/apache/juneau/releng/rest/ReleaseRest.java
index ca1c7e4de2..4a5e0a31f2 100644
--- a/src/main/java/org/apache/juneau/releng/rest/ReleaseRest.java
+++ b/src/main/java/org/apache/juneau/releng/rest/ReleaseRest.java
@@ -21,7 +21,7 @@ import java.util.List;
 import org.apache.juneau.commons.inject.Bean;
 import org.apache.juneau.http.Path;
 import org.apache.juneau.http.response.NotFound;
-import org.apache.juneau.marshall.html.HtmlSerializer;
+import org.apache.juneau.marshall.json.JsonSerializer;
 import org.apache.juneau.rest.server.Rest;
 import org.apache.juneau.rest.server.RestGet;
 import org.apache.juneau.rest.server.RestRequest;
@@ -38,7 +38,7 @@ import org.apache.juneau.rest.server.views.RibbonAction;
 import org.apache.juneau.rest.server.views.ViewDef;
 import org.apache.juneau.rest.server.views.ViewDef.DataMode;
 import org.apache.juneau.rest.server.views.ViewDef.Dir;
-import org.apache.juneau.rest.server.views.ViewTable;
+import org.apache.juneau.rest.server.views.ViewSlot;
 import org.apache.juneau.rest.server.views.ViewsMixin;
 import org.apache.juneau.releng.release.Release;
 import org.apache.juneau.releng.release.ReleaseListService;
@@ -50,10 +50,11 @@ import jakarta.servlet.http.HttpServletRequest;
  *
  * <p>
  * Built on the {@code juneau-rest-server-views} rich-view toolkit: {@link #releasesView()} declares the typed
- * {@link ViewDef} (columns + renderers + ribbon), {@link #page(RestRequest)} emits its {@link ViewTable} shell as trusted markup
- * into the FreeMarker template, and {@link #data()} serves the {@code DataTablesResults} envelope via
- * {@link ProtocolQueryable} + the view's {@link ViewDef#queryableSettings() queryable settings}. The four runtime
- * assets are served by the composed {@link ViewsMixin} at this resource's mount.
+ * {@link ViewDef} (columns + renderers + ribbon), {@link #page(RestRequest)} serves an empty slot that
+ * {@code JuneauViews.regions.mount} fills from {@link #releasesViewEnvelope(RestRequest)}, and {@link #data()}
+ * serves the {@code DataTablesResults} envelope via {@link ProtocolQueryable} + the view's
+ * {@link ViewDef#queryableSettings() queryable settings}. Toolkit assets are served by the composed
+ * {@link ViewsMixin} at this resource's mount.
  */
 @Rest(path = "/releases", title = "Releases", responseProcessors = FreemarkerViewRenderer.class, mixins = ViewsMixin.class)
 public class ReleaseRest extends BasicRestResource {
@@ -91,7 +92,7 @@ public class ReleaseRest extends BasicRestResource {
 	 * rendered Status/Stage pills (emitting the shared {@code .tag.<domain>.<value>} classes the app's console-ui
 	 * palette themes), timestamp/date columns, and a copy/csv export + column-search + status quick-filter + refresh
 	 * ribbon. Data arrives via ajax draws against {@link #data()}. Static (no instance state) so {@code AdminRest}
-	 * can reuse this same declarative definition when composing the {@code Admin} tab page.
+	 * can reuse this same declarative definition when mounting the Admin Releases pair.
 	 */
 	static ViewDef releasesView() {
 		return ViewDef.create("releases")
@@ -124,30 +125,21 @@ public class ReleaseRest extends BasicRestResource {
 			.build();
 	}
 
-	/** Human page — the rich-view table shell (emitted as trusted markup) + JSON sidecar, hydrated by the toolkit JS. */
+	/** Human page — empty {@code #releases} slot; the table is mounted from {@link #releasesViewEnvelope}. */
 	@RestGet("/")
 	public View page(RestRequest req) {
-		var markup = HtmlSerializer.DEFAULT_SIMPLE_SQ.toString(ViewTable.of(req, releasesView()));
-		return ConsolePage.of("releases", req)
-			.attr("viewTable", markup)
-			.attr("viewsCssUrl", asset(req, ViewsMixin.VIEWS_CSS_PATH))
-			.attr("configCssUrl", asset(req, ViewsMixin.CONFIG_CSS_PATH))
-			.attr("rendersJsUrl", asset(req, ViewsMixin.RENDERS_JS_PATH))
-			.attr("iconsJsUrl", asset(req, ViewsMixin.ICONS_JS_PATH))
-			.attr("ribbonJsUrl", asset(req, ViewsMixin.RIBBON_JS_PATH))
-			.attr("viewsJsUrl", asset(req, ViewsMixin.VIEWS_JS_PATH))
-			.attr("configJsUrl", asset(req, ViewsMixin.CONFIG_JS_PATH));
+		return TableSlotPage.of("releases", req, "releases", MOUNT + "/view");
 	}
 
 	/**
-	 * Resolves a toolkit asset to an absolute, cache-busted URL for the FreeMarker head block via the
-	 * request-aware {@link ViewsMixin#viewAssetUrl(RestRequest, String)}, resolved per-request against
-	 * this resource's actual mount/context path rather than a hardcoded string-replace of the {@code servlet:}
-	 * scheme &mdash; the FreeMarker template is rendered outside Juneau's {@code HtmlDoc} URL-resolution, which is
-	 * why the URL must already be resolved before it reaches the template.
+	 * Slot envelope for the Releases table (Admin pair and this standalone page share this URL).
+	 *
+	 * @param req The current request. Must not be {@code null}.
+	 * @return SLOT_META wrapping {@link #releasesView()}.
 	 */
-	private static String asset(RestRequest req, String path) {
-		return ViewsMixin.viewAssetUrl(req, path);
+	@RestGet(path = "/view", produces = "application/json", serializers = JsonSerializer.class)
+	public ViewSlot releasesViewEnvelope(RestRequest req) {
+		return ViewSlot.envelope(req, releasesView());
 	}
 
 	/**
diff --git a/src/main/java/org/apache/juneau/releng/rest/TableSlotPage.java b/src/main/java/org/apache/juneau/releng/rest/TableSlotPage.java
new file mode 100644
index 0000000000..54a34c4c88
--- /dev/null
+++ b/src/main/java/org/apache/juneau/releng/rest/TableSlotPage.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.juneau.releng.rest;
+
+import java.util.LinkedHashMap;
+import org.apache.juneau.marshall.marshaller.Json;
+import org.apache.juneau.rest.server.RestRequest;
+import org.apache.juneau.rest.server.view.freemarker.FreemarkerView;
+import org.apache.juneau.rest.server.views.ViewsMixin;
+
+/**
+ * Shared empty-slot + toolkit-asset attrs for pages that mount a table via
+ * {@code JuneauViews.regions.mount({ [slotId]: { table: url } })}.
+ */
+final class TableSlotPage {
+
+	/** Static page script that reads {@code #rm-table-slot} and calls {@code regions.mount}. */
+	static final String JS_URL = "/js/table-slot.js";
+
+	private TableSlotPage() {}
+
+	/**
+	 * Seeds the named template with CSRF (via {@link ConsolePage}), slot hookup JSON, and toolkit asset URLs.
+	 *
+	 * @param template The FreeMarker template name.
+	 * @param req The current request. Must not be {@code null}.
+	 * @param slotId HTML id of the empty slot. Must not be blank.
+	 * @param tableUrl Same-origin envelope URL. Must not be blank.
+	 * @return The view; callers may add further attrs.
+	 */
+	static FreemarkerView of(String template, RestRequest req, String slotId, String tableUrl) {
+		return ConsolePage.of(template, req)
+			.attr("slotId", slotId)
+			.attr("tableUrl", tableUrl)
+			.attr("tableSlotJson", slotJson(slotId, tableUrl))
+			.attr("tableSlotJsUrl", JS_URL)
+			.attr("viewsCssUrl", asset(req, ViewsMixin.VIEWS_CSS_PATH))
+			.attr("configCssUrl", asset(req, ViewsMixin.CONFIG_CSS_PATH))
+			.attr("rendersJsUrl", asset(req, ViewsMixin.RENDERS_JS_PATH))
+			.attr("iconsJsUrl", asset(req, ViewsMixin.ICONS_JS_PATH))
+			.attr("ribbonJsUrl", asset(req, ViewsMixin.RIBBON_JS_PATH))
+			.attr("viewsJsUrl", asset(req, ViewsMixin.VIEWS_JS_PATH))
+			.attr("configJsUrl", asset(req, ViewsMixin.CONFIG_JS_PATH))
+			.attr("regionsJsUrl", asset(req, ViewsMixin.REGIONS_JS_PATH))
+			.attr("helpersJsUrl", asset(req, ViewsMixin.HELPERS_JS_PATH));
+	}
+
+	static String slotJson(String slotId, String tableUrl) {
+		var m = new LinkedHashMap<String,String>();
+		m.put("slotId", slotId);
+		m.put("tableUrl", tableUrl);
+		return Json.DEFAULT.write(m);
+	}
+
+	private static String asset(RestRequest req, String path) {
+		return ViewsMixin.viewAssetUrl(req, path);
+	}
+}
diff --git a/src/main/resources/static/js/table-slot.js b/src/main/resources/static/js/table-slot.js
new file mode 100644
index 0000000000..92fcf2a259
--- /dev/null
+++ b/src/main/resources/static/js/table-slot.js
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Mounts a Juneau table into an empty HTML slot.
+ *
+ * Reads #rm-table-slot JSON {slotId, tableUrl} and calls
+ * JuneauViews.regions.mount({ [slotId]: { table: tableUrl } }). Fetch/handshake
+ * failures are left to the toolkit, which paints a banner in the slot.
+ */
+(function () {
+	'use strict';
+
+	function parseCfg() {
+		var el = document.getElementById('rm-table-slot');
+		if (!el)
+			return null;
+		return JSON.parse(el.textContent || 'null');
+	}
+
+	function mount() {
+		var cfg = parseCfg();
+		if (!cfg || !cfg.slotId || !cfg.tableUrl)
+			return;
+		var regions = globalThis.JuneauViews && globalThis.JuneauViews.regions;
+		if (!regions || typeof regions.mount !== 'function') {
+			if (globalThis.console && console.error)
+				console.error('JuneauViews.regions.mount is not available; table slot was not mounted.');
+			return;
+		}
+		var map = {};
+		map[cfg.slotId] = { table: cfg.tableUrl };
+		regions.mount(map);
+	}
+
+	if (document.readyState === 'loading')
+		document.addEventListener('DOMContentLoaded', mount);
+	else
+		mount();
+})();
diff --git a/src/main/resources/templates/admin.ftlh b/src/main/resources/templates/admin.ftlh
index 0fba204ec6..9e275821c9 100644
--- a/src/main/resources/templates/admin.ftlh
+++ b/src/main/resources/templates/admin.ftlh
@@ -22,15 +22,17 @@
             <h2>Admin</h2>
         </div>
 
-        <#-- TODO-399 Phase C: the composed tabs/sub-tabs page shell (PageTable.of(...) via AdminRest.page()),
-             pre-serialized server-side and inserted as trusted markup: ?no_esc suppresses this .ftlh's HTML
-             auto-escaping so the emitted [data-juneau-page]/<table data-juneau-view>/<script> markup is not
-             double-escaped. juneau-pages.js hash-routes the tabs and lazy-inits each panel's DataTable on first
-             activation via juneau-views.js's initTable.
-             TODO-445n: the toolkit now owns table-only horizontal scroll, so the old .rm-table-scroll wrapper is
-             removed — an outer overflow-x:auto here would clip the paging-pill menu (INV-2) and can produce two
-             horizontal scrollbars. Each panel's ViewTable wrapper carries data-juneau-layout="wide". -->
-        ${pageTable?no_esc}
+        <nav class="juneau-page-nav" aria-label="Admin">
+            <div class="juneau-page-nav-sections">
+                <a class="juneau-page-nav-section" href="/rest/admin/releases" aria-current="page">Admin</a>
+            </div>
+            <div class="juneau-page-nav-children">
+                <a class="juneau-page-nav-child" href="/rest/admin/releases"<#if selectedChild == 'releases'> aria-current="page"</#if>>Releases</a>
+                <a class="juneau-page-nav-child" href="/rest/admin/credentials"<#if selectedChild == 'credentials'> aria-current="page"</#if>>Credentials</a>
+            </div>
+        </nav>
+        <div id="${slotId}"></div>
+        <script type="application/json" id="rm-table-slot">${tableSlotJson?no_esc}</script>
     </div>
 </#macro>
 <#include "base.ftlh">
diff --git a/src/main/resources/templates/base.ftlh b/src/main/resources/templates/base.ftlh
index 5f0d9b2c60..ec50d97ec1 100644
--- a/src/main/resources/templates/base.ftlh
+++ b/src/main/resources/templates/base.ftlh
@@ -48,15 +48,14 @@
          feature-detects window.JSZip / window.pdfMake and lights the Excel/PDF icon buttons. -->
     <link rel="stylesheet" href="https://cdn.datatables.net/buttons/3.2.0/css/buttons.dataTables.min.css">
     <#-- First-party toolkit base .tag chip stylesheet (neutral); the app's console-ui palette themes the same
-         .tag.<domain>.<value> classes. This same stylesheet also carries the TODO-399 Phase C .jc-tab/.jc-panel
-         page-shell base rules the Admin tab's juneau-pages.js runtime relies on. -->
+         .tag.<domain>.<value> classes. The same stylesheet also carries .juneau-page-nav rules for the Admin pair pages. -->
     <link rel="stylesheet" href="${viewsCssUrl!'/rest/releases/juneau-views.css'}">
     <link rel="stylesheet" href="${configCssUrl!'/rest/releases/juneau-config.css'}">
     </#if>
     <link rel="stylesheet" href="/juneau-console/chrome.css">
     <link rel="stylesheet" href="/css/chrome.css">
 </head>
-<body>
+<body data-juneau-csrf="${csrfToken}" data-juneau-csrf-header="${csrfHeader}">
     <header class="jc-header">
         <div class="jc-brand">
             <div class="jc-logo" role="img" aria-label="Apache Juneau"></div>
@@ -89,12 +88,11 @@
 
     <#if activeTab?? && (activeTab == 'releases' || activeTab == 'admin')>
     <#-- Caller-provided libs: self-hosted jQuery + DataTables, plus the DataTables Buttons extension (copy/csv)
-         from CDN. Then the first-party toolkit runtime in load order renders -> icons -> ribbon -> views;
-         juneau-views.js scans table[data-juneau-view] on DOMContentLoaded and hydrates the server-side table +
-         ribbon. juneau-icons.js MUST load before juneau-ribbon.js: the ribbon/paging-pill buttons resolve their
-         glyphs from the icon registry (window.JuneauViews.icons) at build time, so the registry must already
-         exist when juneau-ribbon.js/juneau-views.js render them. The old hand-rolled /js/releases.js DataTable
-         init is retired — the toolkit owns initialization now. -->
+         from CDN. Then the first-party toolkit runtime in load order renders -> icons -> ribbon -> views ->
+         config -> regions -> helpers; juneau-regions.js mounts empty slots via
+         JuneauViews.regions.mount({ [slotId]: { table: url } }). juneau-icons.js MUST load before juneau-ribbon.js:
+         the ribbon/paging-pill buttons resolve their glyphs from the icon registry (window.JuneauViews.icons) at
+         build time, so the registry must already exist when juneau-ribbon.js/juneau-views.js render them. -->
     <script src="/datatables/jquery.min.js"></script>
     <script src="/datatables/dataTables.min.js"></script>
     <script src="https://cdn.datatables.net/buttons/3.2.0/js/dataTables.buttons.min.js"></script>
@@ -110,12 +108,11 @@
     <script src="${ribbonJsUrl!'/rest/releases/juneau-ribbon.js'}"></script>
     <script src="${viewsJsUrl!'/rest/releases/juneau-views.js'}"></script>
     <script src="${configJsUrl!'/rest/releases/juneau-config.js'}"></script>
+    <script src="${regionsJsUrl!'/rest/releases/juneau-regions.js'}"></script>
+    <script src="${helpersJsUrl!'/rest/releases/juneau-helpers.js'}"></script>
+    <#if tableSlotJsUrl??>
+    <script src="${tableSlotJsUrl}"></script>
     </#if>
-    <#if activeTab?? && activeTab == 'admin'>
-    <#-- TODO-399 Phase C: the opt-in tabs/sub-tabs page runtime — only the Admin tab's composed PageDef page loads
-         this; the single-view Releases tab above does not. Must load after juneau-views.js (it calls the
-         now-public NS.init.initTable to lazy-init a panel's DataTable on first activation). -->
-    <script src="${pagesJsUrl!'/rest/admin/juneau-pages.js'}"></script>
     </#if>
     <#if activeTab?? && activeTab == 'credentials'>
     <script src="/js/credentials.js"></script>
diff --git a/src/main/resources/templates/releases.ftlh b/src/main/resources/templates/releases.ftlh
index 801f319ab3..1562287efd 100644
--- a/src/main/resources/templates/releases.ftlh
+++ b/src/main/resources/templates/releases.ftlh
@@ -22,16 +22,8 @@
             <h2>All Releases</h2>
         </div>
 
-        <#-- Rich-view toolkit table + JSON sidecar, pre-serialized server-side (ReleaseRest.page via
-             ViewTable.of) and inserted as trusted markup: ?no_esc suppresses this .ftlh's HTML auto-escaping
-             so the emitted <table data-juneau-view>/<script> markup is not double-escaped. juneau-views.js
-             hydrates it into a server-side DataTable + ribbon on DOMContentLoaded.
-             TODO-445n: the toolkit now owns table-only horizontal scroll (juneau-views.css / the flex
-             .dt-layout-cell), so the old .rm-table-scroll wrapper is removed — an outer overflow-x:auto here
-             would clip the paging-pill menu (INV-2) and can produce two horizontal scrollbars. The
-             ViewTable wrapper <div> now sits as a direct child of .jc-card and its data-juneau-layout="wide"
-             stamp widens the card to full content width. -->
-        ${viewTable?no_esc}
+        <div id="${slotId}"></div>
+        <script type="application/json" id="rm-table-slot">${tableSlotJson?no_esc}</script>
     </div>
 </#macro>
 <#include "base.ftlh">
diff --git a/src/test/java/org/apache/juneau/releng/rest/AdminRestTest.java b/src/test/java/org/apache/juneau/releng/rest/AdminRestTest.java
index d731eaa117..27fcfe206b 100644
--- a/src/test/java/org/apache/juneau/releng/rest/AdminRestTest.java
+++ b/src/test/java/org/apache/juneau/releng/rest/AdminRestTest.java
@@ -24,17 +24,14 @@ import java.nio.charset.StandardCharsets;
 
 import org.apache.juneau.commons.inject.StackOverlay;
 import org.apache.juneau.commons.utils.IoUtils;
-import org.apache.juneau.marshall.html.HtmlSerializer;
 import org.apache.juneau.rest.mock.MockRestClient;
+import org.apache.juneau.rest.server.filter.LoopbackBoundary;
 import org.apache.juneau.rest.server.views.PageTable;
-import org.apache.juneau.rest.server.views.ViewTable;
 import org.apache.juneau.rest.server.views.ViewsMixin;
 import org.junit.jupiter.api.Test;
 
 /**
- * The RM {@code Admin} tab composes the existing Releases/Credentials
- * {@link org.apache.juneau.rest.server.views.ViewDef ViewDef}s into one {@link org.apache.juneau.rest.server.views.PageDef PageDef}
- * page, rendered by {@link PageTable} and served through {@link AdminRest}.
+ * Admin pair pages: path-per-child, local {@code .juneau-page-nav}, empty slots, no {@code PageTable} shell.
  */
 class AdminRestTest {
 
@@ -49,72 +46,72 @@ class AdminRestTest {
 		return MockRestClient.builder(new AdminRest()).overridingBeanStore(new StackOverlay()).build();
 	}
 
-	// -----------------------------------------------------------------------------------------------------------
-	// Task 10: PageDef composition
-	// -----------------------------------------------------------------------------------------------------------
-
 	@Test
-	void a01_adminPageComposesOneTabPerExistingView() {
-		var page = AdminRest.adminPage();
-		assertEquals("admin", page.id);
-		assertEquals(2, page.tabs.size());
-		assertEquals("releases", page.tabs.get(0).id);
-		assertEquals("releases", page.tabs.get(0).view.id);
-		assertEquals("credentials", page.tabs.get(1).id);
-		assertEquals("credentials", page.tabs.get(1).view.id);
+	void a01_defaultAdminUrlRedirectsToReleasesPair() throws Exception {
+		try (var client = client()) {
+			try (var resp = client.request("GET", "/").run()) {
+				assertEquals(302, resp.getStatusCode());
+				assertEquals(AdminRest.RELEASES_URL, resp.header("Location").getValue());
+			}
+		}
 	}
 
 	@Test
-	void a02_adminPageBuildsWithoutValidationErrors() {
-		// PageDef.build() rejects duplicate tab ids / duplicate referenced ViewDef ids (Phase C task 2 rules);
-		// building here (rather than only in AdminRest.adminPage()) proves the composition is actually valid,
-		// not just that adminPage() happens not to throw.
-		assertDoesNotThrow(AdminRest::adminPage);
+	void a02_unknownAdminChildIs404() throws Exception {
+		try (var client = client()) {
+			try (var resp = client.request("GET", "/foo").run()) {
+				assertEquals(404, resp.getStatusCode());
+			}
+		}
 	}
 
-	// -----------------------------------------------------------------------------------------------------------
-	// Task 11: PageTable wiring + no regression to the standalone per-view output
-	// -----------------------------------------------------------------------------------------------------------
-
 	@Test
-	void b01_pageServesPageTableShellWithBothPanelsAndSidecars() throws Exception {
+	void b01_releasesPairServesAuthorNavAndEmptySlot() throws Exception {
 		try (var client = client()) {
-			try (var resp = client.request("GET", "/").run()) {
+			try (var resp = client.request("GET", "/releases").run()) {
 				assertEquals(200, resp.getStatusCode());
 				var body = resp.getBodyAsString();
-				assertTrue(body.contains("data-juneau-page='admin'"), "Missing page shell: " + body);
-				assertTrue(body.contains("data-juneau-view='releases'"), "Missing releases panel: " + body);
-				assertTrue(body.contains("data-juneau-view='credentials'"), "Missing credentials panel: " + body);
-				assertTrue(body.contains("juneau-page:admin"), "Missing PAGE_META sidecar: " + body);
-				assertTrue(body.contains("juneau-view:releases"), "Missing releases VIEW_META sidecar: " + body);
-				assertTrue(body.contains("juneau-view:credentials"), "Missing credentials VIEW_META sidecar: " + body);
+				assertAdminShell(body, "releases", "/rest/releases/view");
+				assertTrue(body.contains("href=\"/rest/admin/releases\" aria-current=\"page\">Releases</a>"), body);
+				assertFalse(body.contains("href=\"/rest/admin/credentials\" aria-current=\"page\""), body);
 			}
 		}
 	}
 
 	@Test
-	void b02_wrappedReleasesViewMarkupIsByteForByteIdenticalToStandalone() {
-		var wrapped = HtmlSerializer.DEFAULT_SIMPLE_SQ.toString(PageTable.of(AdminRest.adminPage()));
-		var standalone = HtmlSerializer.DEFAULT_SIMPLE_SQ.toString(ViewTable.of(ReleaseRest.releasesView()));
-		assertTrue(wrapped.contains(standalone),
-			"Page-wrapped Releases view markup diverged from the standalone ViewTable.of(...) output.");
+	void b02_credentialsPairServesAuthorNavAndEmptySlot() throws Exception {
+		try (var client = client()) {
+			try (var resp = client.request("GET", "/credentials").run()) {
+				assertEquals(200, resp.getStatusCode());
+				var body = resp.getBodyAsString();
+				assertAdminShell(body, "credentials", "/rest/credentials/view");
+				assertTrue(body.contains("href=\"/rest/admin/credentials\" aria-current=\"page\">Credentials</a>"), body);
+				assertFalse(body.contains("href=\"/rest/admin/releases\" aria-current=\"page\">Releases</a>"), body);
+			}
+		}
 	}
 
 	@Test
-	void b03_wrappedCredentialsViewMarkupIsByteForByteIdenticalToStandalone() {
-		var wrapped = HtmlSerializer.DEFAULT_SIMPLE_SQ.toString(PageTable.of(AdminRest.adminPage()));
-		var standalone = HtmlSerializer.DEFAULT_SIMPLE_SQ.toString(ViewTable.of(CredentialRest.credentialsView()));
-		assertTrue(wrapped.contains(standalone),
-			"Page-wrapped Credentials view markup diverged from the standalone ViewTable.of(...) output.");
+	void b03_releasesPairMountsTheSharedReleasesEnvelopeUrl() throws Exception {
+		try (var client = client()) {
+			try (var resp = client.request("GET", "/releases").run()) {
+				var body = resp.getBodyAsString();
+				assertTrue(body.contains("\"tableUrl\":\"/rest/releases/view\""), body);
+				assertTrue(body.contains("\"slotId\":\"releases\""), body);
+			}
+		}
 	}
 
 	@Test
-	void b04_assetsAreServedAtTheAdminMount() throws Exception {
+	void b04_regionsAndHelpersAreServedAtTheAdminMount() throws Exception {
 		try (var client = client()) {
-			try (var resp = client.request("GET", ViewsMixin.PAGES_JS_PATH).run()) {
+			try (var resp = client.request("GET", ViewsMixin.REGIONS_JS_PATH).run()) {
 				assertEquals(200, resp.getStatusCode());
 				assertTrue(resp.getBodyAsString().contains("JuneauViews"));
 			}
+			try (var resp = client.request("GET", ViewsMixin.HELPERS_JS_PATH).run()) {
+				assertEquals(200, resp.getStatusCode());
+			}
 			try (var resp = client.request("GET", ViewsMixin.VIEWS_JS_PATH).run()) {
 				assertEquals(200, resp.getStatusCode());
 			}
@@ -128,31 +125,19 @@ class AdminRestTest {
 		}
 	}
 
-	// -----------------------------------------------------------------------------------------------------------
-	// Task 12: the composed page uses the self-contained -views tab shell (PageTable's classes), not hand-rolled
-	// per-page tab markup — verified here structurally (the shell's own marker classes); base.ftlh's nav-link and
-	// asset-include wiring for the Admin tab is exercised end-to-end by b01_pageServesPageTableShellWithBothPanelsAndSidecars
-	// once rendered through the FreeMarker template at /admin.
-	// -----------------------------------------------------------------------------------------------------------
-
 	@Test
-	void c01_composedPageUsesTheSharedTabShellClassesNotBespokeMarkup() {
-		var html = HtmlSerializer.DEFAULT_SIMPLE_SQ.toString(PageTable.of(AdminRest.adminPage()));
-		assertTrue(html.contains("class='" + PageTable.TAB_BAR_CLASS + "'") || html.contains(PageTable.TAB_BAR_CLASS));
-		assertTrue(html.contains(PageTable.TAB_CLASS));
-		assertTrue(html.contains(PageTable.PANEL_CLASS));
+	void c01_releasesPairStampsCsrfOnBody() throws Exception {
+		try (var client = client()) {
+			try (var resp = client.request("GET", "/releases").run()) {
+				var body = resp.getBodyAsString();
+				assertTrue(body.contains("data-juneau-csrf="), body);
+				assertTrue(body.contains("data-juneau-csrf-header=\"" + LoopbackBoundary.DEFAULT_CSRF_HEADER + "\""), body);
+			}
+		}
 	}
 
-	/**
-	 * The app's own cross-resource nav (base.ftlh's {@code .jc-nav}) gains an Admin entry, and the Admin tab pulls
-	 * in the opt-in {@code juneau-pages.js} runtime after {@code juneau-views.js} &mdash; the top-level nav
-	 * mechanism itself (real links across {@code @Rest(children=...)} resources) is intentionally unchanged
-	 * (design doc non-goal: no top-level cross-resource navigation redesign; Phase C owns only the in-page tab
-	 * switch, which {@link #c01_composedPageUsesTheSharedTabShellClassesNotBespokeMarkup()} already verifies uses the
-	 * shared shell classes).
-	 */
 	@Test
-	void c02_baseTemplateWiresTheAdminNavLinkAndPagesRuntime() throws IOException {
+	void c02_baseTemplateWiresTheAdminNavLinkAndRegionsRuntime() throws IOException {
 		String base;
 		try (var in = AdminRestTest.class.getResourceAsStream("/templates/base.ftlh")) {
 			assertNotNull(in, "templates/base.ftlh not found on the test classpath");
@@ -160,16 +145,24 @@ class AdminRestTest {
 		}
 		assertTrue(base.contains("href=\"/rest/admin\""), "Missing Admin nav link: " + base);
 		assertTrue(base.contains("activeTab == 'admin'"), "Missing admin-tab conditional asset wiring: " + base);
-		assertTrue(base.contains("pagesJsUrl"), "Missing juneau-pages.js include for the Admin tab: " + base);
+		assertFalse(base.contains("pagesJsUrl"), "juneau-pages.js must not be included: " + base);
+		assertFalse(base.contains("juneau-pages.js"), "juneau-pages.js must not be referenced: " + base);
+		assertTrue(base.contains("regionsJsUrl"), "Missing juneau-regions.js include: " + base);
+		assertTrue(base.contains("helpersJsUrl"), "Missing juneau-helpers.js include: " + base);
+		assertTrue(base.contains("data-juneau-csrf="), "Missing CSRF ancestor stamp: " + base);
+		assertTrue(base.contains("data-juneau-csrf-header="), "Missing CSRF header stamp: " + base);
 		assertTrue(base.contains("configJsUrl"), "Missing juneau-config.js include: " + base);
 		assertTrue(base.contains("configCssUrl"), "Missing juneau-config.css include: " + base);
 		var viewsJsIdx = base.indexOf("viewsJsUrl");
 		var configJsIdx = base.indexOf("configJsUrl");
-		var pagesJsIdx = base.indexOf("pagesJsUrl");
+		var regionsJsIdx = base.indexOf("regionsJsUrl");
+		var helpersJsIdx = base.indexOf("helpersJsUrl");
 		assertTrue(viewsJsIdx >= 0 && configJsIdx > viewsJsIdx,
 			"juneau-config.js must load after juneau-views.js: " + base);
-		assertTrue(pagesJsIdx > configJsIdx,
-			"juneau-pages.js must load after juneau-config.js: " + base);
+		assertTrue(regionsJsIdx > configJsIdx,
+			"juneau-regions.js must load after juneau-config.js: " + base);
+		assertTrue(helpersJsIdx > regionsJsIdx,
+			"juneau-helpers.js must load after juneau-regions.js: " + base);
 	}
 
 	/**
@@ -192,4 +185,31 @@ class AdminRestTest {
 		assertTrue(iconsIdx < ribbonIdx,
 			"juneau-icons.js must be included before juneau-ribbon.js (icon registry must exist when the ribbon builds its buttons): " + base);
 	}
+
+	@Test
+	void c04_newFixturesContainNoSalesforceTokens() throws IOException {
+		String js;
+		try (var in = AdminRestTest.class.getResourceAsStream("/static/js/table-slot.js")) {
+			assertNotNull(in, "table-slot.js not found on the test classpath");
+			js = new String(IoUtils.readBytes(in), StandardCharsets.UTF_8);
+		}
+		assertFalse(js.contains("slds-"), js);
+		assertFalse(js.toLowerCase().contains("salesforce"), js);
+	}
+
+	private static void assertAdminShell(String body, String slotId, String tableUrl) {
+		assertTrue(body.contains("class=\"juneau-page-nav\""), "Missing page nav: " + body);
+		assertTrue(body.contains("aria-label=\"Admin\""), body);
+		assertTrue(body.contains("id=\"" + slotId + "\""), "Missing empty slot: " + body);
+		assertTrue(body.contains("id=\"rm-table-slot\""), body);
+		assertTrue(body.contains("\"tableUrl\":\"" + tableUrl + "\""), body);
+		assertFalse(body.contains("data-juneau-page"), "Page sidecar leaked: " + body);
+		assertFalse(body.contains("juneau-page:admin"), "PAGE_META leaked: " + body);
+		assertFalse(body.contains(PageTable.TAB_BAR_CLASS), "Tab bar class leaked: " + body);
+		assertFalse(body.contains("juneau-pages.js"), "juneau-pages.js leaked: " + body);
+		assertFalse(body.contains("data-juneau-region"), "Do not stamp data-juneau-region on the table slot: " + body);
+		assertTrue(body.contains("juneau-regions.js"), "Missing regions runtime: " + body);
+		assertTrue(body.contains("/js/table-slot.js"), body);
+		assertFalse(body.contains("slds-"), body);
+	}
 }
diff --git a/src/test/java/org/apache/juneau/releng/rest/CredentialRestTest.java b/src/test/java/org/apache/juneau/releng/rest/CredentialRestTest.java
new file mode 100644
index 0000000000..2e4890c635
--- /dev/null
+++ b/src/test/java/org/apache/juneau/releng/rest/CredentialRestTest.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.juneau.releng.rest;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.file.Path;
+import java.util.EnumMap;
+
+import org.apache.juneau.commons.inject.StackOverlay;
+import org.apache.juneau.commons.secret.InMemorySecretStore;
+import org.apache.juneau.commons.secret.SecretStore;
+import org.apache.juneau.releng.credential.AccountStore;
+import org.apache.juneau.releng.credential.CredentialService;
+import org.apache.juneau.releng.credential.CredentialSpec;
+import org.apache.juneau.rest.mock.MockRestClient;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Credentials envelope JSON lives on this resource without {@code ViewsMixin}; the human page stays the secret-store UI.
+ */
+class CredentialRestTest {
+
+	private CredentialService service;
+
+	@BeforeEach
+	void setUp(@TempDir Path stateDir) {
+		var stores = new EnumMap<CredentialSpec,SecretStore>(CredentialSpec.class);
+		for (var spec : CredentialSpec.values())
+			stores.put(spec, new InMemorySecretStore());
+		service = new CredentialService(stores, new EnumMap<>(CredentialSpec.class), new AccountStore(stateDir));
+	}
+
+	@SuppressWarnings({
+		"resource" // Caller owns and closes the returned MockRestClient (via try-with-resources).
+	})
+	private MockRestClient client() {
+		return MockRestClient.builder(new CredentialRest(service)).overridingBeanStore(new StackOverlay()).build();
+	}
+
+	@Test
+	void a01_viewEnvelopeIsSlotMetaWithoutCsrf() throws Exception {
+		try (var client = client()) {
+			try (var resp = client.request("GET", "/view").header("Accept", "application/json").run()) {
+				assertEquals(200, resp.getStatusCode());
+				ReleaseRestTest.assertSlotEnvelope(resp.getBodyAsString(), "credentials");
+			}
+		}
+	}
+
+	@Test
+	void a02_pageRemainsTheSecretStoreUi() throws Exception {
+		try (var client = client()) {
+			try (var resp = client.request("GET", "/").run()) {
+				assertEquals(200, resp.getStatusCode());
+				var body = resp.getBodyAsString();
+				assertTrue(body.contains("/js/credentials.js"), body);
+				assertFalse(body.contains("class=\"juneau-page-nav\""), body);
+				assertFalse(body.contains("id=\"rm-table-slot\""), body);
+				assertFalse(body.contains("juneau-view:credentials"), body);
+			}
+		}
+	}
+}
diff --git a/src/test/java/org/apache/juneau/releng/rest/ReleaseRestTest.java b/src/test/java/org/apache/juneau/releng/rest/ReleaseRestTest.java
index ebc53708d9..b6eccfe97b 100644
--- a/src/test/java/org/apache/juneau/releng/rest/ReleaseRestTest.java
+++ b/src/test/java/org/apache/juneau/releng/rest/ReleaseRestTest.java
@@ -21,11 +21,15 @@ import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.Mockito.*;
 
 import java.util.List;
+import java.util.Map;
 import org.apache.juneau.commons.inject.StackOverlay;
 import org.apache.juneau.http.response.NotFound;
+import org.apache.juneau.marshall.marshaller.Json;
 import org.apache.juneau.releng.release.Release;
 import org.apache.juneau.releng.release.ReleaseListService;
 import org.apache.juneau.rest.mock.MockRestClient;
+import org.apache.juneau.rest.server.filter.LoopbackBoundary;
+import org.apache.juneau.rest.server.views.ViewSlot;
 import org.junit.jupiter.api.Test;
 
 import jakarta.servlet.http.HttpServletRequest;
@@ -123,6 +127,15 @@ class ReleaseRestTest {
 				assertTrue(ribbonIdx >= 0, "Missing juneau-ribbon.js script include: " + body);
 				assertTrue(iconsIdx < ribbonIdx,
 					"juneau-icons.js must be included before juneau-ribbon.js: " + body);
+				assertTrue(body.contains("id=\"releases\""), "Missing empty releases slot: " + body);
+				assertFalse(body.contains("juneau-view:releases"), "ViewTable sidecar leaked: " + body);
+				assertFalse(body.contains("data-juneau-view"), "Marker table leaked: " + body);
+				assertTrue(body.contains("juneau-regions.js"), "Missing regions runtime: " + body);
+				assertTrue(body.contains("/js/table-slot.js"), body);
+				assertTrue(body.contains("\"tableUrl\":\"/rest/releases/view\""), body);
+				assertTrue(body.contains("data-juneau-csrf="), body);
+				assertTrue(body.contains("data-juneau-csrf-header=\"" + LoopbackBoundary.DEFAULT_CSRF_HEADER + "\""), body);
+				assertFalse(body.contains("class=\"juneau-page-nav\""), "Standalone Releases must not carry Admin page nav: " + body);
 			}
 		}
 	}
@@ -183,4 +196,29 @@ class ReleaseRestTest {
 			}
 		}
 	}
+
+	@Test
+	void d01_viewEnvelopeIsSlotMetaWithoutCsrf() throws Exception {
+		try (var client = client(rest(List.of(release("9.2.1", "RELEASED"))))) {
+			try (var resp = client.request("GET", "/view").header("Accept", "application/json").run()) {
+				assertEquals(200, resp.getStatusCode());
+				var body = resp.getBodyAsString();
+				assertSlotEnvelope(body, "releases");
+			}
+		}
+	}
+
+	@SuppressWarnings({
+		"unchecked" // Json.DEFAULT.read to Map is an unchecked conversion from the raw parser result.
+	})
+	static void assertSlotEnvelope(String body, String viewId) {
+		var slot = Json.DEFAULT.read(body, Map.class);
+		assertEquals(ViewSlot.CONTRACT_VERSION, slot.get("contractVersion"), body);
+		assertEquals("wide", slot.get("layout"), body);
+		assertFalse(slot.containsKey("csrf"), body);
+		var view = (Map<String,Object>) slot.get("view");
+		assertNotNull(view, body);
+		assertEquals("4", view.get("contractVersion"), body);
+		assertEquals(viewId, view.get("id"), body);
+	}
 }