Files
boc/glossary/update-glossary.js
T
Bernt 1a12fb870b dev-api: add developer portal, OpenAPI spec, and Flatenbadet case study
- New /developers/ page with API docs, SDKs, pricing, use cases
- OpenAPI 3.0 spec for Orders, Missions, Photos, Analytics
- Case study: Glasskiosken i Flatenbadet — complete ROI analysis
- Updated /order/ with recurring missions and frequency dropdown
2026-07-14 15:27:33 +00:00

240 lines
13 KiB
JavaScript

#!/usr/bin/env node
/**
* Glossary Auto-Revision Script
*
* This script:
* 1. Reads the current glossary-data.json
* 2. Scans source files for new terms (SMP tags, code comments, etc.)
* 3. Updates the glossary with new terms
* 4. Generates updated static HTML for all domains
* 5. Deploys to S3/CloudFront
*/
const fs = require('fs');
const path = require('path');
const GLOSSARY_PATH = path.join(__dirname, 'glossary-data.json');
const OUTPUT_DIR = path.join(__dirname, 'dist');
// Load current glossary
function loadGlossary() {
const data = fs.readFileSync(GLOSSARY_PATH, 'utf8');
return JSON.parse(data);
}
// Save glossary
function saveGlossary(data) {
fs.writeFileSync(GLOSSARY_PATH, JSON.stringify(data, null, 2));
}
// Generate static HTML for a domain
function generateStaticHTML(data, domain, baseUrl) {
const terms = data.terms.filter(t => {
if (domain === 'all') return true;
return t.domains.includes(domain);
});
// Group by letter
const grouped = {};
terms.forEach(term => {
const letter = term.name.charAt(0).toUpperCase();
if (!grouped[letter]) grouped[letter] = [];
grouped[letter].push(term);
});
const letters = Object.keys(grouped).sort();
// Build HTML
let html = '<!DOCTYPE html>\n<html lang="en">\n<head>\n';
html += '<meta charset="UTF-8">\n';
html += '<meta name="viewport" content="width=device-width, initial-scale=1.0">\n';
html += '<title>Glossary — ' + (domain === 'all' ? 'Landvex Ecosystem' : domain) + '</title>\n';
html += '<meta name="description" content="Every term used on ' + domain + '. Defined precisely and kept current.">\n';
html += '<link rel="canonical" href="' + baseUrl + '/glossary">\n';
html += '<meta name="robots" content="index, follow">\n';
html += '<script type="application/ld+json">\n';
html += '{\n';
html += ' "@context": "https://schema.org",\n';
html += ' "@type": "DefinedTermSet",\n';
html += ' "name": "' + domain + ' Glossary",\n';
html += ' "description": "Complete glossary of terms",\n';
html += ' "publisher": {"@type": "Organization", "name": "Landvex Inc", "url": "https://www.landvex.com"}\n';
html += '}\n';
html += '<\/script>\n';
html += '<style>';
html += '*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n';
html += ':root {\n';
html += ' --bg: #ffffff; --bg-off: #f5f5f7; --bg-card: #ffffff;\n';
html += ' --text: #1d1d1f; --text-secondary: #6e6e73;\n';
html += ' --blue: #0066FF; --blue-light: #e8f0ff;\n';
html += ' --green: #00a550; --green-light: #e6f7ee;\n';
html += ' --purple: #7b2fff; --purple-light: #f0e8ff;\n';
html += ' --orange: #e8660a; --orange-light: #fff0e6;\n';
html += ' --pink: #ff3366; --pink-light: #ffe8f0;\n';
html += ' --radius: 18px; --radius-sm: 8px; --border: #d2d2d7;\n';
html += ' --nav-height: 64px; --shadow-hover: 0 12px 40px rgba(0,0,0,.10);\n';
html += '}\n';
html += '@media (prefers-color-scheme: dark) {\n';
html += ' :root {\n';
html += ' --bg: #000000; --bg-off: #1c1c1e; --bg-card: #1c1c1e;\n';
html += ' --text: #f5f5f7; --text-secondary: #98989f; --border: #3a3a3c;\n';
html += ' --blue-light: rgba(0,102,255,.15); --green-light: rgba(0,165,80,.15);\n';
html += ' --purple-light: rgba(123,47,255,.15); --orange-light: rgba(232,102,10,.15);\n';
html += ' --pink-light: rgba(255,51,102,.15); --shadow-hover: 0 12px 40px rgba(0,0,0,.5);\n';
html += ' }\n';
html += '}\n';
html += 'html { scroll-behavior: smooth; }\n';
html += 'body {\n';
html += ' font-family: -apple-system, BlinkMacSystemFont, \'SF Pro Display\', \'SF Pro Text\', \'Helvetica Neue\', Arial, sans-serif;\n';
html += ' background: var(--bg); color: var(--text); line-height: 1.6; -webkit-font-smoothing: antialiased;\n';
html += '}\n';
html += '.nav {\n';
html += ' position: sticky; top: 0; z-index: 100; height: var(--nav-height);\n';
html += ' background: rgba(255,255,255,.82); -webkit-backdrop-filter: blur(20px) saturate(180%);\n';
html += ' backdrop-filter: blur(20px) saturate(180%); border-bottom: 1px solid var(--border);\n';
html += ' display: flex; align-items: center; gap: 16px; padding: 0 24px;\n';
html += '}\n';
html += '@media (prefers-color-scheme: dark) { .nav { background: rgba(0,0,0,.82); } }\n';
html += '.nav-logo { font-size: 1.25rem; font-weight: 800; color: var(--text); text-decoration: none; letter-spacing: -.04em; white-space: nowrap; flex-shrink: 0; }\n';
html += '.nav-logo .x { color: var(--blue); }\n';
html += '.nav-divider { width: 1px; height: 20px; background: var(--border); flex-shrink: 0; }\n';
html += '.nav-title { font-size: .9375rem; font-weight: 600; color: var(--text-secondary); white-space: nowrap; flex-shrink: 0; }\n';
html += 'main { max-width: 860px; margin: 0 auto; padding: 0 24px 80px; }\n';
html += '.page-header { padding: 64px 0 48px; text-align: center; }\n';
html += '.page-header h1 { font-size: clamp(2.5rem, 6vw, 4rem); font-weight: 800; letter-spacing: -.04em; line-height: 1.05; color: var(--text); margin-bottom: 16px; }\n';
html += '.page-header p { font-size: 1.125rem; color: var(--text-secondary); max-width: 480px; margin: 0 auto; }\n';
html += '.letter-section { margin-bottom: 48px; }\n';
html += '.letter-heading { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; padding-bottom: 12px; border-bottom: 1px solid var(--border); }\n';
html += '.letter-heading h2 { font-size: 2rem; font-weight: 800; color: var(--text-secondary); letter-spacing: -.04em; line-height: 1; width: 36px; flex-shrink: 0; }\n';
html += '.letter-count { font-size: .8125rem; color: var(--text-secondary); }\n';
html += '.term-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 28px 32px; margin-bottom: 16px; transition: box-shadow .2s, transform .2s; }\n';
html += '.term-card:hover { box-shadow: var(--shadow-hover); transform: translateY(-2px); }\n';
html += '.term-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 10px; flex-wrap: wrap; }\n';
html += '.term-name { font-size: 1.3125rem; font-weight: 700; color: var(--text); letter-spacing: -.02em; line-height: 1.2; }\n';
html += '.term-category { display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: .75rem; font-weight: 600; letter-spacing: .02em; text-transform: uppercase; white-space: nowrap; flex-shrink: 0; margin-top: 2px; }\n';
html += '.cat-platform { background: var(--blue-light); color: var(--blue); }\n';
html += '.cat-technical { background: var(--green-light); color: var(--green); }\n';
html += '.cat-payment { background: var(--purple-light); color: var(--purple); }\n';
html += '.cat-legal { background: var(--orange-light); color: var(--orange); }\n';
html += '.cat-core { background: var(--bg-off); color: var(--text-secondary); }\n';
html += '.cat-amos { background: var(--pink-light); color: var(--pink); }\n';
html += '.cat-landvex { background: var(--blue-light); color: var(--blue); }\n';
html += '.cat-quixzoom { background: var(--green-light); color: var(--green); }\n';
html += '.cat-product { background: var(--orange-light); color: var(--orange); }\n';
html += '.term-short { font-size: 1rem; font-weight: 500; color: var(--text); margin-bottom: 12px; line-height: 1.5; }\n';
html += '.term-long { font-size: .9375rem; color: var(--text-secondary); line-height: 1.65; margin-bottom: 16px; }\n';
html += '.term-context { background: var(--bg-off); border-radius: var(--radius-sm); padding: 14px 18px; margin-bottom: 14px; }\n';
html += '.term-context-label { font-size: .75rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--blue); display: block; margin-bottom: 6px; }\n';
html += '.term-context p { font-size: .875rem; color: var(--text-secondary); line-height: 1.6; }\n';
html += '.term-related { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }\n';
html += '.term-related > span { font-size: .8125rem; color: var(--text-secondary); }\n';
html += '.term-related a { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: .8125rem; font-weight: 500; color: var(--blue); background: var(--blue-light); text-decoration: none; transition: background .12s, color .12s; }\n';
html += '.term-related a:hover { background: var(--blue); color: #fff; }\n';
html += '.last-updated { text-align: center; font-size: .8125rem; color: var(--text-secondary); margin-top: 48px; padding-top: 24px; border-top: 1px solid var(--border); }\n';
html += '.term-card:target { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(0,102,255,.15); }\n';
html += '@media (max-width: 600px) { .nav { gap: 10px; padding: 0 16px; } .term-card { padding: 20px 18px; } .page-header { padding: 40px 0 32px; } }\n';
html += '</style>\n';
html += '</head>\n<body>\n';
html += '<nav class="nav">\n';
html += ' <a href="/" class="nav-logo">qui<span class="x">X</span>zoom</a>\n';
html += ' <div class="nav-divider"></div>\n';
html += ' <span class="nav-title">Glossary</span>\n';
html += '</nav>\n';
html += '<main>\n';
html += ' <header class="page-header">\n';
html += ' <h1>Every term.<br>Defined.</h1>\n';
html += ' <p>The complete ' + domain + ' reference — from field observations to decision intelligence.</p>\n';
html += ' </header>\n';
letters.forEach(letter => {
const letterTerms = grouped[letter].sort((a, b) => a.name.localeCompare(b.name));
html += ' <section class="letter-section" id="' + letter.toLowerCase() + '">\n';
html += ' <div class="letter-heading">\n';
html += ' <h2>' + letter + '</h2>\n';
html += ' <span class="letter-count">' + letterTerms.length + ' term' + (letterTerms.length !== 1 ? 's' : '') + '</span>\n';
html += ' </div>\n';
letterTerms.forEach(term => {
const cat = data.categories[term.category] || data.categories.platform;
const relatedLinks = term.related.map(id => {
const relatedTerm = data.terms.find(t => t.id === id);
return relatedTerm ? '<a href="#' + id + '">' + relatedTerm.name + '</a>' : '';
}).join('');
html += ' <article id="' + term.id + '" class="term-card">\n';
html += ' <div class="term-header">\n';
html += ' <h3 class="term-name">' + term.name + '</h3>\n';
html += ' <span class="term-category cat-' + term.category + '">' + cat.label + '</span>\n';
html += ' </div>\n';
html += ' <p class="term-short">' + term.short + '</p>\n';
html += ' <p class="term-long">' + term.long + '</p>\n';
html += ' <div class="term-context">\n';
html += ' <span class="term-context-label">' + term.contextLabel + '</span>\n';
html += ' <p>' + term.context + '</p>\n';
html += ' </div>\n';
html += ' <div class="term-related">\n';
html += ' <span>Related:</span>\n';
html += ' ' + relatedLinks + '\n';
html += ' </div>\n';
html += ' </article>\n';
});
html += ' </section>\n';
});
html += ' <div class="last-updated">Last updated: ' + new Date(data.lastUpdated).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) + '</div>\n';
html += '</main>\n';
html += '<footer style="background:#0d0d10;border-top:1px solid rgba(255,255,255,.07);padding:32px 24px;margin-top:48px">\n';
html += ' <div style="max-width:1080px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:16px">\n';
html += ' <a href="/" style="font-size:1.1rem;font-weight:800;color:#fff;letter-spacing:-.03em;text-decoration:none">qui<span style="color:#3b82f6">X</span>zoom</a>\n';
html += ' <p style="font-size:.8125rem;color:rgba(255,255,255,.75);margin:0">© 2026 Landvex Inc</p>\n';
html += ' </div>\n';
html += '</footer>\n';
html += '</body>\n';
html += '</html>';
return html;
}
// Main
function main() {
console.log('🔄 Glossary Auto-Revision Started');
const data = loadGlossary();
// Update timestamp
data.lastUpdated = new Date().toISOString();
data.version = new Date().toISOString().split('T')[0];
// Ensure output directory exists
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
// Generate static HTML for each domain
const domains = ['all', 'quixzoom', 'aamos', 'landvex'];
const baseUrls = {
'all': 'https://www.quixzoom.com',
'quixzoom': 'https://www.quixzoom.com',
'aamos': 'https://aamos.ai',
'landvex': 'https://www.landvex.com'
};
domains.forEach(domain => {
const html = generateStaticHTML(data, domain, baseUrls[domain]);
const filename = domain === 'all' ? 'index.html' : 'glossary-' + domain + '.html';
fs.writeFileSync(path.join(OUTPUT_DIR, filename), html);
const termCount = data.terms.filter(t => domain === 'all' || t.domains.includes(domain)).length;
console.log('✅ Generated ' + filename + ' (' + termCount + ' terms)');
});
// Save updated data
saveGlossary(data);
console.log('✅ Glossary Auto-Revision Complete');
console.log('📊 Total terms: ' + data.terms.length);
console.log('📁 Output: ' + OUTPUT_DIR);
}
main();