Files
poshmanager/server/services/packageDocService.js
2026-08-09 17:33:11 -05:00

245 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package documentation — turn an Intune deployment (+ optional linked PSADT
// profile and catalog application) into an auto-generated datasheet. Pure: it
// renders a section model to Markdown or self-contained HTML with no IO. The
// controller loads the records and picks the format.
const RETURN_CODE_LABELS = {
success: 'Success',
softReboot: 'Soft reboot',
hardReboot: 'Hard reboot',
retry: 'Retry',
failed: 'Failed'
};
const INTENT_LABELS = {
available: 'Available',
required: 'Required',
uninstall: 'Uninstall',
exclude: 'Excluded'
};
function dash(value) {
const str = value == null ? '' : String(value).trim();
return str === '' ? '—' : str;
}
// Build a format-neutral section model from the loaded records. Each section is
// one of: { rows } definition list, { table } grid, { code } block, or { text }.
export function buildDatasheetModel({ deployment, profile = null, application = null } = {}) {
if (!deployment) throw new Error('deployment is required');
const title = deployment.name || profile?.name || 'PSADT package';
const vendor = profile?.appVendor || application?.vendor || '';
const appName = profile?.appName || deployment.name || '';
const version = profile?.appVersion || application?.version || '';
const arch = profile?.appArch || deployment.requirements?.architecture || '';
const subtitle = [vendor, appName, version].map((part) => String(part || '').trim()).filter(Boolean).join(' ');
const sections = [];
sections.push({
heading: 'Overview',
rows: [
['Package', title],
['Vendor', vendor],
['Application', appName],
['Version', version],
['Architecture', arch],
['App type', deployment.appType],
['Command style', deployment.commandStyle],
['Install behavior', deployment.installBehavior],
['Restart behavior', deployment.restartBehavior],
['Status', deployment.status]
]
});
sections.push({
heading: 'Install & uninstall',
rows: [
['Install command', { code: deployment.installCommand }],
['Uninstall command', { code: deployment.uninstallCommand }]
]
});
sections.push({
heading: 'Detection',
rows: [['Detection type', deployment.detectionType || 'manual']],
code: deployment.detectionRule ? deployment.detectionRule : null
});
const returnCodes = Array.isArray(deployment.returnCodes) ? deployment.returnCodes : [];
sections.push({
heading: 'Return codes',
table: {
columns: ['Code', 'Type', 'Meaning'],
rows: returnCodes.length
? returnCodes.map((rc) => [String(rc.code), RETURN_CODE_LABELS[rc.type] || rc.type || '', rc.meaning || ''])
: []
}
});
const assignments = Array.isArray(deployment.assignments) ? deployment.assignments : [];
sections.push({
heading: 'Assignments',
table: {
columns: ['Ring', 'Intent', 'Group', 'Notes'],
rows: assignments.length
? assignments.map((a) => [a.ring || '', INTENT_LABELS[a.intent] || a.intent || '', a.groupName || '', a.notes || ''])
: []
}
});
const req = deployment.requirements || {};
sections.push({
heading: 'Requirements',
rows: [
['Architecture', req.architecture],
['Minimum OS', req.minOs],
['Disk space (MB)', req.diskSpaceMb ? String(req.diskSpaceMb) : ''],
['Run as 32-bit', req.runAs32Bit ? 'Yes' : 'No']
]
});
sections.push({
heading: 'Source & version',
rows: [
['PSADT profile', profile?.name || deployment.profileName || ''],
['Source folder', deployment.sourceFolder],
['Package file', deployment.intunewinFile],
['Catalog application', application?.name || deployment.applicationName || ''],
['Version source', application ? `${application.versionSource || ''} ${application.versionSourceRef || ''}`.trim() : '']
]
});
if (deployment.notes) {
sections.push({ heading: 'Notes', text: deployment.notes });
}
return { title, subtitle, generatedAt: new Date().toISOString(), sections };
}
function rowValueToMd(value) {
if (value && typeof value === 'object' && 'code' in value) return `\`${dash(value.code).replace(/`/g, '`')}\``;
return dash(value);
}
function renderMarkdown(model) {
const lines = [`# ${model.title}`, ''];
if (model.subtitle) lines.push(`*${model.subtitle}*`, '');
lines.push(`Generated ${model.generatedAt}`, '');
for (const section of model.sections) {
lines.push(`## ${section.heading}`, '');
if (section.rows) {
lines.push('| Field | Value |', '| --- | --- |');
for (const [label, value] of section.rows) {
lines.push(`| ${label} | ${rowValueToMd(value).replace(/\n/g, ' ')} |`);
}
lines.push('');
}
if (section.table) {
const { columns, rows } = section.table;
lines.push(`| ${columns.join(' | ')} |`, `| ${columns.map(() => '---').join(' | ')} |`);
if (rows.length) {
for (const row of rows) lines.push(`| ${row.map((cell) => dash(cell).replace(/\n/g, ' ').replace(/\|/g, '\\|')).join(' | ')} |`);
} else {
lines.push(`| ${columns.map((_, i) => (i === 0 ? '_None_' : '')).join(' | ')} |`);
}
lines.push('');
}
if (section.code) {
lines.push('```', section.code, '```', '');
}
if (section.text) {
lines.push(section.text, '');
}
}
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
}
function esc(value) {
return dash(value)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function rowValueToHtml(value) {
if (value && typeof value === 'object' && 'code' in value) return `<code>${esc(value.code)}</code>`;
return esc(value);
}
function renderHtml(model) {
const parts = [];
for (const section of model.sections) {
parts.push(`<section><h2>${esc(section.heading)}</h2>`);
if (section.rows) {
parts.push('<table class="kv"><tbody>');
for (const [label, value] of section.rows) {
parts.push(`<tr><th>${esc(label)}</th><td>${rowValueToHtml(value)}</td></tr>`);
}
parts.push('</tbody></table>');
}
if (section.table) {
const { columns, rows } = section.table;
parts.push('<table class="grid"><thead><tr>');
parts.push(columns.map((col) => `<th>${esc(col)}</th>`).join(''));
parts.push('</tr></thead><tbody>');
if (rows.length) {
for (const row of rows) {
parts.push('<tr>' + row.map((cell) => `<td>${esc(cell)}</td>`).join('') + '</tr>');
}
} else {
parts.push(`<tr><td class="muted" colspan="${columns.length}">None configured</td></tr>`);
}
parts.push('</tbody></table>');
}
if (section.code) {
parts.push(`<pre><code>${esc(section.code)}</code></pre>`);
}
if (section.text) {
parts.push(`<p>${esc(section.text)}</p>`);
}
parts.push('</section>');
}
return [
'<!doctype html>',
'<html lang="en"><head><meta charset="utf-8">',
`<title>${esc(model.title)} — package datasheet</title>`,
'<style>',
':root{color-scheme:light dark}',
'body{font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:60rem;margin:2rem auto;padding:0 1rem;line-height:1.5}',
'h1{margin-bottom:.25rem}.subtitle{color:#6b7280;margin-top:0}.meta{color:#6b7280;font-size:.85rem}',
'section{margin-top:1.75rem}h2{border-bottom:1px solid #d1d5db;padding-bottom:.25rem}',
'table{border-collapse:collapse;width:100%;margin-top:.5rem}',
'th,td{text-align:left;padding:.4rem .6rem;border:1px solid #d1d5db;vertical-align:top}',
'table.kv th{width:14rem;white-space:nowrap}',
'code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em}',
'pre{background:#f3f4f6;padding:.75rem;border-radius:.4rem;overflow:auto}pre code{font-size:.85em}',
'.muted{color:#6b7280}',
'</style></head><body>',
`<h1>${esc(model.title)}</h1>`,
model.subtitle ? `<p class="subtitle">${esc(model.subtitle)}</p>` : '',
`<p class="meta">Generated ${esc(model.generatedAt)}</p>`,
parts.join('\n'),
'</body></html>',
''
].filter((line) => line !== '').join('\n');
}
// Render a datasheet for a deployment. format: 'md' (default) | 'html'.
export function renderDatasheet(records, { format = 'md' } = {}) {
const model = buildDatasheetModel(records);
const normalized = format === 'html' ? 'html' : 'md';
const content = normalized === 'html' ? renderHtml(model) : renderMarkdown(model);
return {
format: normalized,
contentType: normalized === 'html' ? 'text/html; charset=utf-8' : 'text/markdown; charset=utf-8',
extension: normalized === 'html' ? 'html' : 'md',
title: model.title,
content
};
}