Match page width to sso-manager-node; gate Audit nav to admins

- Dashboard, Sessions, and Audit pages now wrap their content in
  <div class="container mt-4">, matching sso-manager-node/proxy's width
  instead of rendering full-bleed inside the fluid shell.
- Audit's nav entry now carries groups: ['admin'] (utils/ui.js), reusing
  the existing synthetic-admin-group nav-gating convention -- the API
  route was already server-side admin-gated, this closes the last gap by
  hiding the nav link/page for non-admins too.
- app-base.js (byte-identical across the 3 apps): added
  app.util.revealItem() and the --sw-content-offset sticky-positioning
  variable, carried over from the same round of changes in
  sso-manager-node/proxy. Not yet called anywhere in this app -- no
  sticky/reveal use case here yet -- but keeps the shared file in sync.
This commit is contained in:
2026-07-29 22:20:56 -04:00
parent 6771904932
commit 0ef451e15d
7 changed files with 43 additions and 2 deletions
+6
View File
@@ -7,6 +7,12 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; min-height: 100vh;
/* Height of the fixed navbar (plus the update banner, while shown --
see top.ejs's showUpdateBanner/dismissUpdateBanner). Lets an in-page
sticky element offset itself below both fixed elements via
`top: var(--sw-content-offset)` instead of colliding with them at the
viewport's true top:0. */
--sw-content-offset: 4.5rem;
} }
#spa-shell { #spa-shell {
+21
View File
@@ -584,10 +584,31 @@ app.util = (function(app){
document.body.removeChild(element); document.body.removeChild(element);
} }
// Scroll a just-added/-edited element into view and flash its
// background, so the user's eye lands on the row that changed instead of
// it silently appearing/updating somewhere off-screen. Takes a jQuery
// object or a raw DOM node (e.g. jq-repeat's `item.__jq_$el`).
function revealItem(el){
var node = el && el.jquery ? el[0] : el;
if (!node) return;
if (typeof node.scrollIntoView === 'function') {
node.scrollIntoView({behavior: 'smooth', block: 'center'});
}
var prevTransition = node.style.transition;
var prevBg = node.style.backgroundColor;
node.style.transition = 'background-color 1.5s ease';
node.style.backgroundColor = 'var(--bs-success-bg-subtle, #d1e7dd)';
setTimeout(function(){
node.style.backgroundColor = prevBg;
setTimeout(function(){ node.style.transition = prevTransition; }, 1500);
}, 300);
}
return { return {
downloadFile: downloadFile, downloadFile: downloadFile,
getUrlParameter: getUrlParameter, getUrlParameter: getUrlParameter,
escapeHtml: escapeHtml, escapeHtml: escapeHtml,
revealItem: revealItem,
} }
})(app); })(app);
+1 -1
View File
@@ -37,6 +37,6 @@ module.exports = {
nav: [ nav: [
{href: '/dashboard', icon: 'fa-solid fa-gauge-high', label: 'Dashboard', groups: []}, {href: '/dashboard', icon: 'fa-solid fa-gauge-high', label: 'Dashboard', groups: []},
{href: '/sessions', icon: 'fa-solid fa-plug-circle-bolt', label: 'Sessions', groups: []}, {href: '/sessions', icon: 'fa-solid fa-plug-circle-bolt', label: 'Sessions', groups: []},
{href: '/audit', icon: 'fa-solid fa-clipboard-list', label: 'Audit', groups: []}, {href: '/audit', icon: 'fa-solid fa-clipboard-list', label: 'Audit', groups: ['admin']},
], ],
}; };
+2
View File
@@ -1,6 +1,7 @@
<%- include('top') %> <%- include('top') %>
<script type="text/javascript">app.auth.forceLogin();</script> <script type="text/javascript">app.auth.forceLogin();</script>
<div class="container mt-4">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header"><i class="fa-solid fa-clipboard-list me-1"></i> Audit log</div> <div class="card-header"><i class="fa-solid fa-clipboard-list me-1"></i> Audit log</div>
<div class="card-body pb-0"> <div class="card-body pb-0">
@@ -29,6 +30,7 @@
<button class="btn btn-sm btn-outline-secondary" id="next" onclick="changePage(1)">next &rarr;</button> <button class="btn btn-sm btn-outline-secondary" id="next" onclick="changePage(1)">next &rarr;</button>
</div> </div>
</div> </div>
</div>
<script type="text/javascript"> <script type="text/javascript">
var page = 0; var page = 0;
+2
View File
@@ -1,6 +1,7 @@
<%- include('top') %> <%- include('top') %>
<script type="text/javascript">app.auth.forceLogin();</script> <script type="text/javascript">app.auth.forceLogin();</script>
<div class="container mt-4">
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<div class="col-6 col-md-3"> <div class="col-6 col-md-3">
<div class="card shadow-sm text-center"><div class="card-body"> <div class="card shadow-sm text-center"><div class="card-body">
@@ -117,6 +118,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<script type="text/javascript"> <script type="text/javascript">
function rows(sel, list){ function rows(sel, list){
+2
View File
@@ -1,6 +1,7 @@
<%- include('top') %> <%- include('top') %>
<script type="text/javascript">app.auth.forceLogin();</script> <script type="text/javascript">app.auth.forceLogin();</script>
<div class="container mt-4">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fa-solid fa-plug-circle-bolt me-1"></i> Active sessions</span> <span><i class="fa-solid fa-plug-circle-bolt me-1"></i> Active sessions</span>
@@ -13,6 +14,7 @@
</table> </table>
</div> </div>
</div> </div>
</div>
<script type="text/javascript"> <script type="text/javascript">
function loadSessions(){ function loadSessions(){
+9 -1
View File
@@ -82,16 +82,24 @@
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
// --sw-content-offset tracks the same height as #spa-shell's margin-top
// (fixed navbar, plus the update banner while it's shown), so any
// in-page sticky element (e.g. a sticky search/sort bar) can offset
// itself below both fixed elements via `top: var(--sw-content-offset)`
// instead of colliding with them at the viewport's true top:0.
function showUpdateBanner(){ function showUpdateBanner(){
let $nav = $('nav.fixed-top'); let $nav = $('nav.fixed-top');
let $banner = $('#update-banner'); let $banner = $('#update-banner');
$banner.css('top', $nav.outerHeight() + 'px').show(); $banner.css('top', $nav.outerHeight() + 'px').show();
$('#spa-shell').css('margin-top', ($nav.outerHeight() + $banner.outerHeight()) + 'px'); let offset = $nav.outerHeight() + $banner.outerHeight();
$('#spa-shell').css('margin-top', offset + 'px');
document.documentElement.style.setProperty('--sw-content-offset', offset + 'px');
} }
function dismissUpdateBanner(){ function dismissUpdateBanner(){
$('#update-banner').hide(); $('#update-banner').hide();
$('#spa-shell').css('margin-top', ''); $('#spa-shell').css('margin-top', '');
document.documentElement.style.setProperty('--sw-content-offset', $('nav.fixed-top').outerHeight() + 'px');
sessionStorage.setItem('update-banner-dismissed', '1'); sessionStorage.setItem('update-banner-dismissed', '1');
} }