ZDDC/transmittal/js/hydrate.js
ZDDC ea385b5366 Initial commit
ZDDC — Zero Day Document Control. A file-naming convention plus five
single-file HTML tools (archive, transmittal, classifier, mdedit,
landing) and an optional Go HTTP server (zddc-server) with ACL and a
virtual archive index. Self-contained, offline-capable, dependency-free.

See README.md for an overview, AGENTS.md and ARCHITECTURE.md for the
build/release/architecture detail, bootstrap/README.md for the
two-level deployment install pattern, and zddc/README.md for the
HTTP server.
2026-04-27 11:05:47 -05:00

258 lines
12 KiB
JavaScript

(function (app) {
'use strict';
const dom = app.dom;
const json = app.json;
const util = app.util;
/**
* Hydrates static HTML content with data from JSON.
* Called on page load to hide static placeholders and show dynamic content.
*/
function hydrate() {
// Hide static "Not Validated" warning (will be replaced by dynamic validation)
const staticWarning = dom.qs('#signature-status-static');
if (staticWarning) {
staticWarning.hidden = true;
}
// Digest will be populated by security.renderSignaturesList()
// which is called after signature verification
}
/**
* Populates static HTML before saving/publishing.
* This ensures the page displays content even without JavaScript.
*/
async function populateStatic() {
const data = json.parse();
const envelope = data.envelope || {};
const payload = data.payload || {};
const signatures = Array.isArray(envelope.signatures) ? envelope.signatures : [];
const files = Array.isArray(payload.files) ? payload.files : [];
// Populate all form fields with actual values
const fields = {
'#type': payload.type || 'Transmittal',
'#title': payload.title || '',
'#owner-name': payload.client || '',
'#project-name': payload.project || '',
'#project-number': payload.projectNumber || '',
'#tracking-number': payload.trackingNumber || '',
'#date': payload.date || '',
'#from': payload.from || '',
'#to': payload.to || '',
'#purpose': payload.purpose || '',
'#response-due': payload.responseDue || '',
'#subject': payload.subject || '',
'#remarks': payload.remarks || ''
};
Object.keys(fields).forEach(function(selector) {
const el = dom.qs(selector);
if (el) {
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
el.value = fields[selector];
el.setAttribute('value', fields[selector]);
} else {
el.textContent = fields[selector];
}
}
});
var typeDisp = dom.qs('#type-display');
if (typeDisp) { typeDisp.textContent = fields['#type']; }
// Render markdown for static display
if (app.modules.markdown && payload.remarks) {
const remarksRender = dom.qs('#remarks-render');
if (remarksRender) {
remarksRender.innerHTML = app.modules.markdown.render(payload.remarks);
}
}
// Populate table with file data
const SELF_HASH = app.constants.SELF_HASH;
const tbody = dom.qs('.table-wrapper tbody');
if (tbody) {
let html = '';
let rowNum = 0;
// Separate self-entry from regular files
var selfFile = null;
var regularFiles = [];
files.forEach(function(file) {
if (file.sha256 === SELF_HASH) {
selfFile = file;
} else {
regularFiles.push(file);
}
});
// If no explicit self-entry in JSON, synthesize one from payload header
if (!selfFile) {
selfFile = {
trackingNumber: payload.trackingNumber || '',
title: payload.subject || payload.title || '',
revision: payload.date || '',
status: payload.purpose || '',
extension: 'html',
fileSize: 0,
sha256: SELF_HASH
};
}
// Build self-link filename from payload fields
var selfFilename = '';
var dataModule = app.modules.data;
if (dataModule && dataModule.buildFileName) {
selfFilename = dataModule.buildFileName(payload, { extension: 'html' });
}
var selfHref = selfFilename ? encodeURI('./' + selfFilename) : '';
var selfTrackingHtml = selfHref
? '<a href="' + selfHref + '" class="text-gray-500 hover:underline" target="_blank" rel="noopener">' + util.escapeHtml(selfFile.trackingNumber || '') + '</a>'
: util.escapeHtml(selfFile.trackingNumber || '');
// Row 0: self-entry
html += '<tr class="self-entry">' +
'<td class="px-2 py-1 text-center text-gray-400">0</td>' +
'<td class="px-2 py-1 text-gray-500 font-mono">' + selfTrackingHtml + '</td>' +
'<td class="px-2 py-1 text-gray-500">' + util.escapeHtml(selfFile.title || '') + '</td>' +
'<td class="px-2 py-1 text-center text-gray-500 font-mono">' + util.escapeHtml(selfFile.revision || '') + '</td>' +
'<td class="px-2 py-1 text-center text-gray-500">' + util.escapeHtml(selfFile.status || '') + '</td>' +
'<td class="px-2 py-1 text-center text-gray-500">html</td>' +
'<td class="px-2 py-1 text-right text-gray-400">\u2014</td>' +
'<td class="px-2 py-1 font-mono text-[9px] text-gray-400 italic">see above</td>' +
'</tr>';
// Remaining files
regularFiles.forEach(function(file) {
rowNum++;
var isUnmatched = !file.sha256 && !file.fileSize;
var formattedSize = isUnmatched ? '\u2014' : (file.fileSize ? util.formatFileSize(file.fileSize) : '');
var sizeClass = isUnmatched ? 'px-2 py-1 text-right text-gray-400' : 'px-2 py-1 text-right';
var hashContent = isUnmatched ? '<span class="italic text-gray-400">pending</span>' : (file.sha256 ? util.formatShortFileHash(file.sha256) : '');
html += '<tr>' +
'<td class="px-2 py-1 text-center text-gray-400">' + rowNum + '</td>' +
'<td class="px-2 py-1">' + (file.trackingNumber || '') + '</td>' +
'<td class="px-2 py-1">' + (file.title || '') + '</td>' +
'<td class="px-2 py-1 text-center">' + (file.revision || '') + '</td>' +
'<td class="px-2 py-1 text-center">' + (file.status || '') + '</td>' +
'<td class="px-2 py-1 text-center">' + (file.extension || '') + '</td>' +
'<td class="' + sizeClass + '">' + formattedSize + '</td>' +
'<td class="px-2 py-1 font-mono text-[9px]">' + hashContent + '</td>' +
'</tr>';
});
tbody.innerHTML = html || '<tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>';
}
// Populate digest card (static fallback using verify-card format)
const digestDisplay = dom.qs('#digest-display');
if (digestDisplay && envelope.digest) {
const digestedAt = envelope.digestedAt ? new Date(envelope.digestedAt).toLocaleString() : 'Unknown';
digestDisplay.innerHTML =
'<div class="verify-card verify-card--info">' +
'<div class="verify-card__status verify-card__status--info">Digest (SHA-256)</div>' +
'<div class="verify-card__detail"><code>' + envelope.digest + '</code></div>' +
'<div class="verify-card__detail">' + digestedAt + '</div>' +
'</div>';
}
// Populate digest in Advanced section
const digestEl = dom.qs('#digest-info');
if (digestEl && envelope.digest) {
const digestedAt = envelope.digestedAt ? new Date(envelope.digestedAt).toLocaleString() : 'Unknown';
digestEl.innerHTML = '<div class="flex flex-col gap-0.5">' +
'<div><strong>Digest (SHA-256):</strong></div>' +
'<code class="text-[9px] break-all bg-gray-100 px-1 py-0.5 rounded">' + envelope.digest + '</code>' +
'<div class="text-gray-500">Created: ' + digestedAt + '</div>' +
'</div>';
}
// Populate static signature cards
const sigList = dom.qs('#signatures-list');
if (sigList && signatures.length > 0) {
let html = '';
for (let i = 0; i < signatures.length; i++) {
const sig = signatures[i];
const fingerprint = await util.publicKeyFingerprint(sig.publicKeyJwk);
const fpDisplay = fingerprint === null ? 'unavailable' : (fingerprint || 'Unknown');
const signedAt = sig.signedAt ? new Date(sig.signedAt).toLocaleString() : 'Unknown';
html += '<div class="verify-card verify-card--info">' +
'<div class="verify-card__status verify-card__status--info">Signature ' + (i + 1) + '</div>' +
'<div class="verify-card__detail">Key: <code>' + fpDisplay + '</code></div>' +
'<div class="verify-card__detail">' + signedAt + '</div>' +
'</div>';
}
sigList.innerHTML = html;
}
// Show static warning
const staticWarning = dom.qs('#signature-status-static');
if (staticWarning) {
staticWarning.hidden = !(envelope.digest || signatures.length > 0);
}
}
/**
* Populates form fields from current JSON data at runtime.
* Used by verification mode to hydrate the form with loaded data.
*/
function hydrateForm() {
const data = json.parse();
const payload = data.payload || {};
const files = Array.isArray(payload.files) ? payload.files : [];
var fields = {
'#type': payload.type || 'Transmittal',
'#title': payload.title || '',
'#owner-name': payload.client || '',
'#project-name': payload.project || '',
'#project-number': payload.projectNumber || '',
'#tracking-number': payload.trackingNumber || '',
'#date': payload.date || '',
'#from': payload.from || '',
'#to': payload.to || '',
'#purpose': payload.purpose || '',
'#response-due': payload.responseDue || '',
'#subject': payload.subject || '',
'#remarks': payload.remarks || ''
};
Object.keys(fields).forEach(function (selector) {
var el = dom.qs(selector);
if (el) {
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
el.value = fields[selector];
} else {
el.textContent = fields[selector];
}
}
});
var typeDisp2 = dom.qs('#type-display');
if (typeDisp2) { typeDisp2.textContent = fields['#type']; }
// Render markdown
if (app.modules.markdown && payload.remarks) {
var remarksRender = dom.qs('#remarks-render');
if (remarksRender) {
remarksRender.innerHTML = app.modules.markdown.render(payload.remarks);
}
}
// Render email fields
if (app.modules.emailTags) {
if (app.modules.emailTags.render) { app.modules.emailTags.render(); }
if (app.modules.emailTags.renderFrom) { app.modules.emailTags.renderFrom(); }
}
// Visibility
if (app.modules.visibility && app.modules.visibility.applyFieldVisibility) {
app.modules.visibility.applyFieldVisibility();
}
}
app.modules.hydrate = {
hydrate: hydrate,
hydrateForm: hydrateForm,
populateStatic: populateStatic
};
app.registerInit(function () {
hydrate();
});
})(window.transmittalApp);