Skip to content

Commit 1f6244e

Browse files
committed
Split NIST viewer pages by product into separate directories
Reorganize the NIST viewer to generate product-specific pages in separate subdirectories instead of using a single set of pages with localStorage-based product switching. This improves performance and clarity. Changes: Generator (generate_nist_viewer.py): - Generate pages in product-specific subdirectories: rhel8/, rhel9/, rhel10/ - Embed only that product's data in each page (reduces file size from 7.5MB to 2.5MB per page) - Add CURRENT_PRODUCT constant to each page - Create product selector with links to other product directories - Generate redirect index.html that points to rhel9 by default Template changes: - _shared_header.html: Replace dropdown selector with static product links - Remove localStorage-based product switching - Replace getCurrentProduct() with CURRENT_PRODUCT constant - Simplify data access since each page only has one product's data Benefits: - 67% smaller file sizes per page (only one product's data embedded) - Faster page loads (less data to parse) - Clearer separation between products - Direct URLs to specific products (e.g., rhel9/index.html) - No client-side state management needed Structure: build/nist-controls-viewer/ index.html (redirects to rhel9/) rhel8/ index.html, controls.html, gaps.html, etc. rhel9/ index.html, controls.html, gaps.html, etc. rhel10/ index.html, controls.html, gaps.html, etc. Product selector in header shows: "Product: RHEL8 | RHEL9 | RHEL10" with current product in bold
1 parent 283870c commit 1f6244e

8 files changed

Lines changed: 85 additions & 81 deletions

File tree

utils/nist_sync/generate_nist_viewer.py

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,7 @@ def generate_html_viewer(output_dir: Path, templates_dir: Path, viewer_data: Dic
228228

229229
# Read shared components
230230
shared_styles = (templates_dir / '_shared_styles.html').read_text()
231-
shared_header = (templates_dir / '_shared_header.html').read_text()
232-
233-
# Embed the JSON data
234-
json_data = json.dumps(viewer_data, indent=2)
235-
embedded_data_script = f'const EMBEDDED_DATA = {json_data};'
231+
shared_header_template = (templates_dir / '_shared_header.html').read_text()
236232

237233
# List of page templates to generate
238234
pages = [
@@ -244,29 +240,82 @@ def generate_html_viewer(output_dir: Path, templates_dir: Path, viewer_data: Dic
244240
'family.html'
245241
]
246242

247-
# Generate each page
248-
for page in pages:
249-
template_file = templates_dir / page
250-
251-
if not template_file.exists():
252-
print(f"Warning: Template not found: {template_file}")
253-
continue
243+
# Generate pages for each product in separate subdirectories
244+
for product in viewer_data['products'].keys():
245+
product_dir = output_dir / product
246+
product_dir.mkdir(parents=True, exist_ok=True)
254247

255-
# Read template
256-
html_content = template_file.read_text()
257-
258-
# Replace placeholders
259-
html_content = html_content.replace('<!-- SHARED_STYLES_PLACEHOLDER -->', f'<style>{shared_styles}</style>')
260-
html_content = html_content.replace('<!-- SHARED_HEADER_PLACEHOLDER -->', shared_header)
261-
html_content = html_content.replace('/* DATA_PLACEHOLDER */', embedded_data_script)
248+
# Create product-specific data (only this product's data)
249+
product_data = {
250+
'products': {product: viewer_data['products'][product]},
251+
'statistics': {product: viewer_data['statistics'][product]},
252+
'families': viewer_data['families']
253+
}
262254

263-
# Write output file
264-
output_file = output_dir / page
265-
output_file.write_text(html_content)
266-
print(f"Generated: {output_file}")
255+
# Embed the JSON data for this product
256+
json_data = json.dumps(product_data, indent=2)
257+
embedded_data_script = f'const EMBEDDED_DATA = {json_data};\nconst CURRENT_PRODUCT = "{product}";'
258+
259+
# Create product selector links for header
260+
all_products = list(viewer_data['products'].keys())
261+
product_links = []
262+
for prod in all_products:
263+
if prod == product:
264+
product_links.append(f'<strong>{prod.upper()}</strong>')
265+
else:
266+
product_links.append(f'<a href="../{prod}/index.html" style="color: #0366d6; text-decoration: none;">{prod.upper()}</a>')
267+
268+
product_selector_html = ' | '.join(product_links)
269+
270+
# Update shared header with product selector
271+
shared_header = shared_header_template.replace(
272+
'<!-- PRODUCT_SELECTOR_PLACEHOLDER -->',
273+
f'<div style="margin-left: auto; font-size: 14px;">Product: {product_selector_html}</div>'
274+
)
275+
276+
# Generate each page for this product
277+
for page in pages:
278+
template_file = templates_dir / page
279+
280+
if not template_file.exists():
281+
print(f"Warning: Template not found: {template_file}")
282+
continue
283+
284+
# Read template
285+
html_content = template_file.read_text()
286+
287+
# Replace placeholders
288+
html_content = html_content.replace('<!-- SHARED_STYLES_PLACEHOLDER -->', f'<style>{shared_styles}</style>')
289+
html_content = html_content.replace('<!-- SHARED_HEADER_PLACEHOLDER -->', shared_header)
290+
html_content = html_content.replace('/* DATA_PLACEHOLDER */', embedded_data_script)
291+
292+
# Write output file
293+
output_file = product_dir / page
294+
output_file.write_text(html_content)
295+
print(f"Generated: {output_file}")
296+
297+
# Create an index.html that redirects to rhel9 by default
298+
default_product = 'rhel9' if 'rhel9' in viewer_data['products'] else list(viewer_data['products'].keys())[0]
299+
redirect_html = f'''<!DOCTYPE html>
300+
<html lang="en">
301+
<head>
302+
<meta charset="UTF-8">
303+
<meta http-equiv="refresh" content="0; url=./{default_product}/index.html">
304+
<title>NIST 800-53 Control Viewer</title>
305+
</head>
306+
<body>
307+
<p>Redirecting to <a href="./{default_product}/index.html">{default_product.upper()} viewer</a>...</p>
308+
</body>
309+
</html>'''
310+
311+
(output_dir / 'index.html').write_text(redirect_html)
312+
print(f"Generated redirect: {output_dir / 'index.html'}")
267313

268314
print(f"\nMulti-page viewer generated in: {output_dir}")
269-
print(f"Open {output_dir / 'index.html'} in a web browser to view.")
315+
print(f"Product-specific viewers:")
316+
for product in viewer_data['products'].keys():
317+
print(f" - {product.upper()}: {output_dir / product / 'index.html'}")
318+
print(f"\nOpen {output_dir / 'index.html'} in a web browser (redirects to {default_product.upper()}).")
270319

271320

272321
def main():

utils/nist_sync/templates/_shared_header.html

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
<header>
2-
<h1><a href="index.html">NIST 800-53 Control Viewer</a></h1>
3-
<div class="header-controls">
4-
<div class="product-selector">
5-
<label for="product-select">Product:</label>
6-
<select id="product-select">
7-
<!-- Populated by JavaScript -->
8-
</select>
9-
</div>
2+
<div style="display: flex; align-items: center; justify-content: space-between;">
3+
<h1><a href="index.html">NIST 800-53 Control Viewer</a></h1>
4+
<!-- PRODUCT_SELECTOR_PLACEHOLDER -->
105
</div>
116
</header>
127

@@ -21,29 +16,8 @@ <h1><a href="index.html">NIST 800-53 Control Viewer</a></h1>
2116
</nav>
2217

2318
<script>
24-
// Initialize product selector and navigation state
19+
// Highlight current page in navigation
2520
document.addEventListener('DOMContentLoaded', function() {
26-
const productSelect = document.getElementById('product-select');
27-
const currentProduct = localStorage.getItem('selected-product') || 'rhel9';
28-
29-
if (typeof EMBEDDED_DATA !== 'undefined' && EMBEDDED_DATA.products) {
30-
Object.keys(EMBEDDED_DATA.products).forEach(product => {
31-
const option = document.createElement('option');
32-
option.value = product;
33-
option.textContent = product.toUpperCase();
34-
if (product === currentProduct) {
35-
option.selected = true;
36-
}
37-
productSelect.appendChild(option);
38-
});
39-
40-
productSelect.addEventListener('change', function() {
41-
localStorage.setItem('selected-product', this.value);
42-
location.reload();
43-
});
44-
}
45-
46-
// Highlight current page in navigation
4721
const currentPage = window.location.pathname.split('/').pop() || 'index.html';
4822
const navLinks = {
4923
'index.html': 'nav-dashboard',

utils/nist_sync/templates/control-detail.html

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,6 @@
203203
return urlParams.get(param);
204204
}
205205

206-
function getCurrentProduct() {
207-
return localStorage.getItem('selected-product') || 'rhel9';
208-
}
209206

210207
function getTodos(controlId) {
211208
const todos = localStorage.getItem(`todos-${controlId}`);
@@ -230,7 +227,7 @@
230227
return;
231228
}
232229

233-
const product = getCurrentProduct();
230+
const product = CURRENT_PRODUCT;
234231
const controls = EMBEDDED_DATA.products[product]?.controls || [];
235232
const control = controls.find(c => c.id === controlId.toLowerCase());
236233

utils/nist_sync/templates/controls.html

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,6 @@ <h4>Gap Status</h4>
202202
let allControls = [];
203203
let filteredControls = [];
204204

205-
function getCurrentProduct() {
206-
return localStorage.getItem('selected-product') || 'rhel9';
207-
}
208205

209206
function initializeControls() {
210207
if (!EMBEDDED_DATA || !EMBEDDED_DATA.products) {
@@ -213,7 +210,7 @@ <h4>Gap Status</h4>
213210
return;
214211
}
215212

216-
const product = getCurrentProduct();
213+
const product = CURRENT_PRODUCT;
217214
allControls = EMBEDDED_DATA.products[product]?.controls || [];
218215

219216
renderFamilyFilters();

utils/nist_sync/templates/family.html

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,6 @@ <h3>Controls in this Family</h3>
179179
return urlParams.get(param);
180180
}
181181

182-
function getCurrentProduct() {
183-
return localStorage.getItem('selected-product') || 'rhel9';
184-
}
185182

186183
function initializeFamilyPage() {
187184
if (!EMBEDDED_DATA || !EMBEDDED_DATA.products) {
@@ -200,7 +197,7 @@ <h3>Controls in this Family</h3>
200197
}
201198

202199
function renderFamilyList() {
203-
const product = getCurrentProduct();
200+
const product = CURRENT_PRODUCT;
204201
const controls = EMBEDDED_DATA.products[product]?.controls || [];
205202

206203
// Calculate stats for each family
@@ -290,7 +287,7 @@ <h3>Controls in this Family</h3>
290287
}
291288

292289
function renderFamilyDetail(familyId) {
293-
const product = getCurrentProduct();
290+
const product = CURRENT_PRODUCT;
294291
const controls = EMBEDDED_DATA.products[product]?.controls || [];
295292
const family = EMBEDDED_DATA.families.find(f => f.id === familyId);
296293

utils/nist_sync/templates/gaps.html

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,6 @@ <h3>Gaps by Control Family</h3>
184184
<script>
185185
/* DATA_PLACEHOLDER */
186186

187-
function getCurrentProduct() {
188-
return localStorage.getItem('selected-product') || 'rhel9';
189-
}
190187

191188
function renderGapAnalysis() {
192189
if (!EMBEDDED_DATA || !EMBEDDED_DATA.products) {
@@ -195,7 +192,7 @@ <h3>Gaps by Control Family</h3>
195192
return;
196193
}
197194

198-
const product = getCurrentProduct();
195+
const product = CURRENT_PRODUCT;
199196
const controls = EMBEDDED_DATA.products[product]?.controls || [];
200197
const gaps = controls.filter(c => !c.has_rules);
201198

utils/nist_sync/templates/index.html

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,14 @@ <h3>Coverage by Control Family</h3>
101101
<script>
102102
/* DATA_PLACEHOLDER */
103103

104-
function getCurrentProduct() {
105-
return localStorage.getItem('selected-product') || 'rhel9';
106-
}
107-
108104
function renderDashboard() {
109-
if (!EMBEDDED_DATA || !EMBEDDED_DATA.products) {
105+
if (!EMBEDDED_DATA || !EMBEDDED_DATA.products || !CURRENT_PRODUCT) {
110106
document.querySelector('.container').innerHTML =
111107
'<div class="empty-state"><h3>Error loading data</h3></div>';
112108
return;
113109
}
114110

115-
const product = getCurrentProduct();
111+
const product = CURRENT_PRODUCT;
116112
const stats = EMBEDDED_DATA.statistics[product];
117113
const controls = EMBEDDED_DATA.products[product]?.controls || [];
118114

utils/nist_sync/templates/statistics.html

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,6 @@ <h3>Statistics by Control Family</h3>
199199
<script>
200200
/* DATA_PLACEHOLDER */
201201

202-
function getCurrentProduct() {
203-
return localStorage.getItem('selected-product') || 'rhel9';
204-
}
205202

206203
function renderStatistics() {
207204
if (!EMBEDDED_DATA || !EMBEDDED_DATA.products) {
@@ -210,7 +207,7 @@ <h3>Statistics by Control Family</h3>
210207
return;
211208
}
212209

213-
const product = getCurrentProduct();
210+
const product = CURRENT_PRODUCT;
214211
const stats = EMBEDDED_DATA.statistics[product];
215212
const controls = EMBEDDED_DATA.products[product]?.controls || [];
216213

0 commit comments

Comments
 (0)