75c57092f8
Lint / JS (eslint) (push) Successful in 10s
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 <noreply@anthropic.com>
156 lines
5.6 KiB
HTML
156 lines
5.6 KiB
HTML
{#
|
|
LOTUSGUILD TERMINAL DESIGN SYSTEM — Flask/Jinja2 Base Template
|
|
Extend this in every page template:
|
|
|
|
{% extends "base.html" %}
|
|
{% block title %}Dashboard{% endblock %}
|
|
{% block active_nav %}dashboard{% endblock %}
|
|
{% block content %}
|
|
… your page HTML …
|
|
{% endblock %}
|
|
{% block scripts %}
|
|
<script nonce="{{ nonce }}">
|
|
lt.sortTable.init('my-table');
|
|
</script>
|
|
{% endblock %}
|
|
|
|
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, APP_SUBTITLE, etc.
|
|
- Pass `nav_links` list of dicts defining navigation
|
|
|
|
Context processor example:
|
|
@app.context_processor
|
|
def inject_globals():
|
|
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">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}Dashboard{% endblock %} — {{ config.get('app_name', 'LotusGuild') }}</title>
|
|
<meta name="description" content="LotusGuild infrastructure management">
|
|
|
|
<!-- Unified design system CSS -->
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='../web_template/base.css') }}">
|
|
<!-- App-specific CSS -->
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
|
|
|
<link rel="icon" href="{{ url_for('static', filename='favicon.png') }}" type="image/png">
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Boot overlay -->
|
|
<div id="lt-boot" class="lt-boot-overlay"
|
|
data-app-name="{{ config.get('app_name', 'APP') | upper }}"
|
|
style="display:none">
|
|
<pre id="lt-boot-text" class="lt-boot-text"></pre>
|
|
</div>
|
|
|
|
<!-- =========================================================
|
|
HEADER
|
|
========================================================= -->
|
|
<header class="lt-header">
|
|
<div class="lt-header-left">
|
|
|
|
<div class="lt-brand">
|
|
<a href="{{ url_for('index') }}" class="lt-brand-title" style="text-decoration:none">
|
|
{{ config.get('app_name', 'APP') | upper }}
|
|
</a>
|
|
<span class="lt-brand-subtitle">{{ config.get('app_subtitle', 'LotusGuild Infrastructure') }}</span>
|
|
</div>
|
|
|
|
<nav class="lt-nav" aria-label="Main navigation">
|
|
{% set active = self.active_nav() | default('') %}
|
|
{% 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>
|
|
|
|
<div class="lt-header-right">
|
|
{% if user.name or user.username %}
|
|
<span class="lt-header-user">{{ user.name or user.username }}</span>
|
|
{% endif %}
|
|
{% if 'admin' in user.groups %}
|
|
<span class="lt-badge-admin">admin</span>
|
|
{% endif %}
|
|
</div>
|
|
</header>
|
|
|
|
<!-- =========================================================
|
|
MAIN CONTENT
|
|
========================================================= -->
|
|
<main class="lt-main lt-container">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
<!-- =========================================================
|
|
SCRIPTS
|
|
All <script> tags MUST carry the nonce attribute for CSP.
|
|
========================================================= -->
|
|
|
|
<!-- Runtime config injected by the server -->
|
|
<script nonce="{{ nonce }}">
|
|
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 -->
|
|
<script nonce="{{ nonce }}" src="{{ url_for('static', filename='../web_template/base.js') }}"></script>
|
|
|
|
<!-- App JS -->
|
|
<script nonce="{{ nonce }}" src="{{ url_for('static', filename='app.js') }}"></script>
|
|
|
|
{% block scripts %}{% endblock %}
|
|
|
|
</body>
|
|
</html>
|
|
|
|
{# ---------------------------------------------------------------
|
|
active_nav block — override in each page template:
|
|
|
|
{% block active_nav %}dashboard{% endblock %}
|
|
|
|
Value must match a 'key' in your nav_links list.
|
|
--------------------------------------------------------------- #}
|
|
{% block active_nav %}{% endblock %}
|