From 75c57092f84e6ab2134dee20abd440dd41cc9980 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 18 Apr 2026 13:53:29 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20make=20layout=20templates=20generic=20?= =?UTF-8?q?=E2=80=94=20remove=20app-specific=20nav=20and=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit php/layout.php: nav is now data-driven via $navLinks array (supports top-level links, dropdowns, adminOnly flag); removed tinker_tickets hardcoded nav items; moved APP_CONFIG note to comment python/base.html: nav driven by nav_links list from context; removed gandalf-specific routes (links_page, inspector, suppressions_page); removed APP_CONFIG.ticketWebUrl from shared script block; added nav_links format documentation in header comment Co-Authored-By: Claude Sonnet 4.6 --- php/layout.php | 57 ++++++++++++++++++++++++++++----------------- python/base.html | 60 +++++++++++++++++++++++++++++------------------- 2 files changed, 73 insertions(+), 44 deletions(-) diff --git a/php/layout.php b/php/layout.php index 1e24b25..ecac3f4 100644 --- a/php/layout.php +++ b/php/layout.php @@ -17,8 +17,14 @@ * $nonce string CSP nonce from SecurityHeadersMiddleware::getNonce() * $currentUser array ['username', 'name', 'is_admin', 'groups'] * $pageTitle string Page suffix - * $activeNav string Which nav link is active ('dashboard','tickets',etc.) + * $activeNav string Which nav key is active — must match a $navLinks entry * $config array From config/config.php + * $navLinks array Navigation items: + * [['href' => '/path', 'key' => 'mykey', 'label' => 'My Page'], ...] + * Nested (dropdown): + * ['label' => 'Admin', 'key' => 'admin', 'adminOnly' => true, 'children' => [ + * ['href' => '/admin/users', 'label' => 'Users'], + * ]] */ // Defensive defaults @@ -27,6 +33,7 @@ $currentUser = $currentUser ?? []; $pageTitle = $pageTitle ?? 'Dashboard'; $activeNav = $activeNav ?? ''; $config = $config ?? []; +$navLinks = $navLinks ?? []; $isAdmin = $currentUser['is_admin'] ?? false; ?> <!DOCTYPE html> @@ -40,7 +47,7 @@ $isAdmin = $currentUser['is_admin'] ?? false; <!-- Unified design system CSS --> <link rel="stylesheet" href="/web_template/base.css"> <!-- App-specific CSS (extends base, never overrides variables without good reason) --> - <link rel="stylesheet" href="/assets/css/app.css?v=<?php echo $config['CSS_VERSION'] ?? '20260314'; ?>"> + <link rel="stylesheet" href="/assets/css/app.css?v=<?php echo $config['CSS_VERSION'] ?? '1'; ?>"> <link rel="icon" href="/assets/images/favicon.png" type="image/png"> </head> @@ -67,25 +74,31 @@ $isAdmin = $currentUser['is_admin'] ?? false; </div> <nav class="lt-nav" aria-label="Main navigation"> - <a href="/" class="lt-nav-link <?php echo $activeNav === 'dashboard' ? 'active' : ''; ?>">Dashboard</a> - <a href="/tickets" class="lt-nav-link <?php echo $activeNav === 'tickets' ? 'active' : ''; ?>">Tickets</a> + <?php foreach ($navLinks as $link): ?> + <?php + $skipAdminOnly = !empty($link['adminOnly']) && !$isAdmin; + if ($skipAdminOnly) continue; + ?> - <?php if ($isAdmin): ?> - <div class="lt-nav-dropdown"> - <a href="#" class="lt-nav-link <?php echo str_starts_with($activeNav, 'admin') ? 'active' : ''; ?>"> - Admin ▾ - </a> - <ul class="lt-nav-dropdown-menu"> - <li><a href="/admin/templates">Templates</a></li> - <li><a href="/admin/workflow">Workflow</a></li> - <li><a href="/admin/recurring-tickets">Recurring</a></li> - <li><a href="/admin/custom-fields">Custom Fields</a></li> - <li><a href="/admin/user-activity">User Activity</a></li> - <li><a href="/admin/audit-log">Audit Log</a></li> - <li><a href="/admin/api-keys">API Keys</a></li> - </ul> - </div> - <?php endif; ?> + <?php if (!empty($link['children'])): ?> + <?php $parentActive = str_starts_with($activeNav, $link['key']); ?> + <div class="lt-nav-dropdown"> + <a href="#" class="lt-nav-link <?php echo $parentActive ? 'active' : ''; ?>"> + <?php echo htmlspecialchars($link['label']); ?> ▾ + </a> + <ul class="lt-nav-dropdown-menu"> + <?php foreach ($link['children'] as $child): ?> + <li><a href="<?php echo htmlspecialchars($child['href']); ?>"><?php echo htmlspecialchars($child['label']); ?></a></li> + <?php endforeach; ?> + </ul> + </div> + <?php else: ?> + <a href="<?php echo htmlspecialchars($link['href']); ?>" + class="lt-nav-link <?php echo $activeNav === $link['key'] ? 'active' : ''; ?>"> + <?php echo htmlspecialchars($link['label']); ?> + </a> + <?php endif; ?> + <?php endforeach; ?> </nav> </div> @@ -130,6 +143,8 @@ $isAdmin = $currentUser['is_admin'] ?? false; username: <?php echo json_encode($currentUser['username'] ?? ''); ?>, isAdmin: <?php echo $isAdmin ? 'true' : 'false'; ?>, }; + // App-specific config: set window.APP_CONFIG in your app's own <script> block, + // not here. This file is shared across all apps. </script> <!-- Unified design system JS --> @@ -137,7 +152,7 @@ $isAdmin = $currentUser['is_admin'] ?? false; <!-- App-specific JS (cache-busted) --> <script nonce="<?php echo $nonce; ?>" - src="/assets/js/app.js?v=<?php echo $config['JS_VERSION'] ?? '20260314'; ?>"> + src="/assets/js/app.js?v=<?php echo $config['JS_VERSION'] ?? '1'; ?>"> </script> <!-- Per-page inline JS goes here in the including view, e.g.: --> diff --git a/python/base.html b/python/base.html index 252a68f..91d33da 100644 --- a/python/base.html +++ b/python/base.html @@ -17,7 +17,8 @@ Required Flask setup (app.py): - Pass `nonce` into every render_template() call via a context processor - Pass `user` dict from _get_user() helper - - Pass `config` dict with APP_NAME, etc. + - Pass `config` dict with APP_NAME, APP_SUBTITLE, etc. + - Pass `nav_links` list of dicts defining navigation Context processor example: @app.context_processor @@ -25,6 +26,16 @@ import base64, os nonce = base64.b64encode(os.urandom(16)).decode() return dict(nonce=nonce, user=_get_user(), config=_config()) + + nav_links format (pass from route or context processor): + nav_links = [ + {'href': url_for('index'), 'key': 'dashboard', 'label': 'Dashboard'}, + {'href': url_for('settings'), 'key': 'settings', 'label': 'Settings'}, + # Admin-only dropdown: + {'label': 'Admin', 'key': 'admin', 'admin_only': True, 'children': [ + {'href': url_for('admin_users'), 'label': 'Users'}, + ]}, + ] #} <!DOCTYPE html> <html lang="en"> @@ -64,24 +75,28 @@ </div> <nav class="lt-nav" aria-label="Main navigation"> - {# Each page sets {% block active_nav %}pagename{% endblock %} #} {% set active = self.active_nav() | default('') %} - <a href="{{ url_for('index') }}" - class="lt-nav-link {% if active == 'dashboard' %}active{% endif %}"> - Dashboard - </a> - <a href="{{ url_for('links_page') }}" - class="lt-nav-link {% if active == 'links' %}active{% endif %}"> - Link Debug - </a> - <a href="{{ url_for('inspector') }}" - class="lt-nav-link {% if active == 'inspector' %}active{% endif %}"> - Inspector - </a> - <a href="{{ url_for('suppressions_page') }}" - class="lt-nav-link {% if active == 'suppressions' %}active{% endif %}"> - Suppressions - </a> + {% for link in nav_links | default([]) %} + {% if not link.get('admin_only') or 'admin' in user.groups %} + {% if link.get('children') %} + <div class="lt-nav-dropdown"> + <a href="#" class="lt-nav-link {% if active.startswith(link.key) %}active{% endif %}"> + {{ link.label }} ▾ + </a> + <ul class="lt-nav-dropdown-menu"> + {% for child in link.children %} + <li><a href="{{ child.href }}">{{ child.label }}</a></li> + {% endfor %} + </ul> + </div> + {% else %} + <a href="{{ link.href }}" + class="lt-nav-link {% if active == link.key %}active{% endif %}"> + {{ link.label }} + </a> + {% endif %} + {% endif %} + {% endfor %} </nav> </div> @@ -107,17 +122,16 @@ All <script> tags MUST carry the nonce attribute for CSP. ========================================================= --> - <!-- Runtime config (no CSRF needed for Gandalf — SameSite=Strict) --> + <!-- Runtime config injected by the server --> <script nonce="{{ nonce }}"> - window.APP_CONFIG = { - ticketWebUrl: {{ config.get('ticket_api', {}).get('web_url', 'https://t.lotusguild.org/ticket/') | tojson }}, - }; window.CURRENT_USER = { username: {{ user.username | tojson }}, name: {{ (user.name or user.username) | tojson }}, groups: {{ user.groups | tojson }}, isAdmin: {{ ('admin' in user.groups) | lower }}, }; + // App-specific config: set window.APP_CONFIG in your app's own template block, + // not here. This file is shared across all apps. </script> <!-- Unified design system JS --> @@ -136,6 +150,6 @@ {% block active_nav %}dashboard{% endblock %} - Values: dashboard | links | inspector | suppressions + Value must match a 'key' in your nav_links list. --------------------------------------------------------------- #} {% block active_nav %}{% endblock %}