247 lines
7.7 KiB
JavaScript
247 lines
7.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Generate documentation screenshots for every admin/user route and each tab.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execSync } = require('child_process');
|
|
const puppeteer = require('puppeteer');
|
|
|
|
const args = process.argv.slice(2);
|
|
const getArg = (key, fallback) => {
|
|
const arg = args.find((item) => item.startsWith(`--${key}=`));
|
|
return arg ? arg.split('=').slice(1).join('=') : fallback;
|
|
};
|
|
const hasFlag = (flag) => args.includes(`--${flag}`);
|
|
|
|
const CONFIG = {
|
|
baseUrl: getArg('base-url', 'https://jabali.lan'),
|
|
outputDir: getArg('output-dir', '/var/www/jabali/docs/screenshots/full'),
|
|
pagesJson: getArg('pages-json', '/var/www/jabali/tests/docs-pages.json'),
|
|
admin: {
|
|
email: getArg('admin-email', 'admin@jabali.lan'),
|
|
password: getArg('admin-password', 'q1w2E#R$'),
|
|
path: getArg('admin-path', '/jabali-admin'),
|
|
},
|
|
user: {
|
|
path: getArg('user-path', '/jabali-panel'),
|
|
},
|
|
impersonateUserId: getArg('impersonate-user-id', '9'),
|
|
skipAdmin: hasFlag('skip-admin'),
|
|
skipUser: hasFlag('skip-user'),
|
|
fullPage: hasFlag('full-page'),
|
|
onlyMissing: hasFlag('only-missing'),
|
|
};
|
|
|
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
function slugify(value) {
|
|
return value
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '');
|
|
}
|
|
|
|
function loadPages() {
|
|
return JSON.parse(fs.readFileSync(CONFIG.pagesJson, 'utf-8'));
|
|
}
|
|
|
|
function queryId(table) {
|
|
try {
|
|
const out = execSync(
|
|
`sqlite3 /var/www/jabali/database/database.sqlite "select id from ${table} limit 1;"`,
|
|
{ encoding: 'utf-8' }
|
|
).trim();
|
|
return out || null;
|
|
} catch (err) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function applyRecordIds(uri) {
|
|
if (!uri.includes('{record}')) return uri;
|
|
const resourceMap = {
|
|
'jabali-admin/users': 'users',
|
|
'jabali-admin/hosting-packages': 'hosting_packages',
|
|
'jabali-admin/geo-block-rules': 'geo_block_rules',
|
|
'jabali-admin/webhook-endpoints': 'webhook_endpoints',
|
|
};
|
|
const base = uri.split('/{record}')[0];
|
|
const table = resourceMap[base];
|
|
if (!table) return uri;
|
|
const id = queryId(table);
|
|
if (!id) return uri;
|
|
return uri.replace('{record}', id);
|
|
}
|
|
|
|
async function loginPanel(page, panelName, basePath, credentials) {
|
|
console.log(`Logging in to ${panelName} panel...`);
|
|
await page.goto(`${CONFIG.baseUrl}${basePath}/login`, { waitUntil: 'networkidle0' });
|
|
await wait(1500);
|
|
await page.type('input[type="email"]', credentials.email);
|
|
await page.type('input[type="password"]', credentials.password);
|
|
await page.click('button[type="submit"]');
|
|
await wait(4000);
|
|
const currentUrl = page.url();
|
|
if (currentUrl.includes('/login')) {
|
|
throw new Error(`${panelName} login failed - still on login page`);
|
|
}
|
|
console.log(`${panelName} login successful!\n`);
|
|
}
|
|
|
|
async function loginUserViaImpersonation(page) {
|
|
if (!CONFIG.impersonateUserId) {
|
|
throw new Error('Missing --impersonate-user-id for user screenshots');
|
|
}
|
|
await loginPanel(page, 'Admin', CONFIG.admin.path, CONFIG.admin);
|
|
const impersonateUrl = `${CONFIG.baseUrl}/impersonate/start/${CONFIG.impersonateUserId}`;
|
|
console.log(`Impersonating user via: ${impersonateUrl}`);
|
|
await page.goto(impersonateUrl, { waitUntil: 'networkidle0' });
|
|
await wait(2000);
|
|
const currentUrl = page.url();
|
|
if (currentUrl.includes('/login')) {
|
|
throw new Error('Impersonation failed - still on login page');
|
|
}
|
|
console.log('User impersonation successful!\n');
|
|
}
|
|
|
|
async function clickTab(page, tabLabel) {
|
|
const normalized = tabLabel.trim().toLowerCase();
|
|
const clicked = await page.evaluate((label) => {
|
|
const tabs = Array.from(document.querySelectorAll('[role="tab"], button'));
|
|
const match = tabs.find((el) => {
|
|
const text = (el.textContent || '').trim().toLowerCase();
|
|
return text === label;
|
|
}) || tabs.find((el) => {
|
|
const text = (el.textContent || '').trim().toLowerCase();
|
|
return text.includes(label);
|
|
});
|
|
if (match) {
|
|
match.click();
|
|
return true;
|
|
}
|
|
return false;
|
|
}, normalized);
|
|
if (clicked) {
|
|
await wait(1500);
|
|
}
|
|
return clicked;
|
|
}
|
|
|
|
async function captureRoute(context, entry, prefix) {
|
|
const page = await context.newPage();
|
|
await page.setViewport({ width: 1400, height: 900 });
|
|
|
|
const uri = applyRecordIds(entry.uri);
|
|
const url = `${CONFIG.baseUrl}/${uri}`.replace(/\/+$/, '');
|
|
const baseName = `${prefix}-${slugify(uri.replace(prefix === 'admin' ? 'jabali-admin' : 'jabali-panel', '')) || 'home'}`;
|
|
const filename = `${baseName}.png`;
|
|
const filepath = path.join(CONFIG.outputDir, filename);
|
|
|
|
const skipBase = CONFIG.onlyMissing && fs.existsSync(filepath);
|
|
|
|
try {
|
|
await page.goto(url, { waitUntil: 'networkidle0', timeout: 45000 });
|
|
await wait(2000);
|
|
|
|
if (!skipBase) {
|
|
await page.screenshot({ path: filepath, fullPage: CONFIG.fullPage });
|
|
console.log(`Saved: ${filepath}`);
|
|
}
|
|
|
|
if (entry.tabs && entry.tabs.length) {
|
|
for (const tab of entry.tabs) {
|
|
const tabFile = `${baseName}--tab-${slugify(tab)}.png`;
|
|
const tabPath = path.join(CONFIG.outputDir, tabFile);
|
|
if (CONFIG.onlyMissing && fs.existsSync(tabPath)) {
|
|
continue;
|
|
}
|
|
const clicked = await clickTab(page, tab);
|
|
if (!clicked) {
|
|
console.log(` Tab not found: ${tab}`);
|
|
continue;
|
|
}
|
|
await page.screenshot({ path: tabPath, fullPage: CONFIG.fullPage });
|
|
console.log(` Tab saved: ${tabPath}`);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.log(`Error capturing ${url}: ${err.message}`);
|
|
} finally {
|
|
await page.close();
|
|
}
|
|
}
|
|
|
|
async function captureAuthPages() {
|
|
const pages = loadPages();
|
|
const authUris = pages.filter((entry) => entry.uri.includes('login') || entry.uri.includes('password-reset') || entry.uri.includes('two-factor'));
|
|
if (!authUris.length) return;
|
|
|
|
const browser = await puppeteer.launch({
|
|
headless: 'new',
|
|
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
});
|
|
|
|
const context = await browser.createBrowserContext();
|
|
for (const entry of authUris) {
|
|
const prefix = entry.panel === 'admin' ? 'admin' : 'user';
|
|
await captureRoute(context, entry, prefix);
|
|
}
|
|
await browser.close();
|
|
}
|
|
|
|
async function main() {
|
|
fs.mkdirSync(CONFIG.outputDir, { recursive: true });
|
|
const pages = loadPages();
|
|
|
|
await captureAuthPages();
|
|
|
|
const browser = await puppeteer.launch({
|
|
headless: 'new',
|
|
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
});
|
|
|
|
if (!CONFIG.skipAdmin) {
|
|
const context = await browser.createBrowserContext();
|
|
const page = await context.newPage();
|
|
await page.setViewport({ width: 1400, height: 900 });
|
|
await loginPanel(page, 'Admin', CONFIG.admin.path, CONFIG.admin);
|
|
await page.close();
|
|
|
|
for (const entry of pages.filter((e) => e.panel === 'admin')) {
|
|
if (entry.uri.includes('login') || entry.uri.includes('password-reset') || entry.uri.includes('two-factor')) {
|
|
continue;
|
|
}
|
|
await captureRoute(context, entry, 'admin');
|
|
}
|
|
|
|
await context.close();
|
|
}
|
|
|
|
if (!CONFIG.skipUser) {
|
|
const context = await browser.createBrowserContext();
|
|
const page = await context.newPage();
|
|
await page.setViewport({ width: 1400, height: 900 });
|
|
await loginUserViaImpersonation(page);
|
|
await page.close();
|
|
|
|
for (const entry of pages.filter((e) => e.panel === 'jabali')) {
|
|
if (entry.uri.includes('login') || entry.uri.includes('password-reset') || entry.uri.includes('two-factor')) {
|
|
continue;
|
|
}
|
|
await captureRoute(context, entry, 'user');
|
|
}
|
|
|
|
await context.close();
|
|
}
|
|
|
|
await browser.close();
|
|
console.log('Documentation screenshots completed.');
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|