4 * A bunch of shorthand to put together common parts of an HTML page.
8 * Some fields may have values outside normal dates, handle them here.
10 * @param {String} otherwise
12 const dateOrNot
= (date
, otherwise
) => {
16 if (typeof date
=== 'number') {
17 date
= new Date(date
);
19 const dateMs
= date
.getTime();
20 if (!Number
.isFinite(dateMs
)
24 return date
.toString();
30 * @param {Number} seconds
33 const secondsToPeriod
= (seconds
) => {
37 const nextResult
= (factor
, label
) => {
38 const r
= factor
? value
% factor : value
;
40 result
.push(`${r} ${label}${r != 1 ? 's' : ''}`);
42 value
= factor
? Math
.floor(value
/ factor
) : value
;
45 nextResult(60, 'second');
46 nextResult(60, 'minute');
47 nextResult(24, 'hour');
48 nextResult(30, 'day');
49 nextResult(undefined, 'month');
52 return result
.join(' ');
57 * Render a topic as a row of details.
58 * @param {Object} topic
59 * @param {Object[]} subscribers
60 * @param {Boolean} detailsLink
63 function renderTopicRow(topic
, subscribers
, detailsLink
= true) {
65 <th scope="row">${detailsLink ? '<a href="topic/' + topic.id + '">' : ''}${topic.url}${detailsLink ? '</a>' : ''}</th>
66 <td>${subscribers.length}</td>
67 <td>${dateOrNot(topic.created, 'Unknown')}</td>
68 <td>${secondsToPeriod(topic.leaseSecondsPreferred)}</td>
69 <td>${secondsToPeriod(topic.leaseSecondsMin)}</td>
70 <td>${secondsToPeriod(topic.leaseSecondsMax)}</td>
71 <td>${topic.publisherValidationUrl ? topic.publisherValidationUrl : 'None'}</td>
72 <td>${topic.isActive}</td>
73 <td>${topic.isDeleted}</td>
74 <td>${dateOrNot(topic.lastPublish, 'Never')}</td>
75 <td>${dateOrNot(topic.contentFetchNextAttempt, 'Next Publish')}</td>
76 <td>${topic.contentFetchAttemptsSinceSuccess}</td>
77 <td>${dateOrNot(topic.contentUpdated, 'Never')}</td>
78 <td>${topic.contentType}</td>
85 * Render the header row for topic details.
88 function renderTopicRowHeader() {
90 <th scope="col">Topic URL</th>
91 <th scope="col">Subscribers</th>
92 <th scope="col">Created</th>
93 <th scope="col">Lease Time Preferred</th>
94 <th scope="col">Lease Time Minimum</th>
95 <th scope="col">Lease Time Maximum</th>
96 <th scope="col">Publisher Validation URL</th>
97 <th scope="col">Active</th>
98 <th scope="col">Deleted</th>
99 <th scope="col">Last Publish Notification</th>
100 <th scope="col">Next Content Fetch</th>
101 <th scope="col">Content Fetch Failures</th>
102 <th scope="col">Content Updated</th>
103 <th scope="col">Content Type</th>
104 <th scope="col">ID</th>
110 * Render a subscription as a row of details.
111 * @param {Object} subscription
114 function renderSubscriptionRow(subscription
) {
116 <td scope="row">${subscription.callback}</td>
117 <td>${dateOrNot(subscription.created, 'Unknown')}</td>
118 <td>${dateOrNot(subscription.verified, 'Never')}</td>
119 <td>${dateOrNot(subscription.expires, 'Never')}</td>
120 <td>${!!subscription.secret}</td>
121 <td>${subscription.signatureAlgorithm}</td>
122 <td>${subscription.httpRemoteAddr}</td>
123 <td>${subscription.httpFrom}</td>
124 <td>${dateOrNot(subscription.contentDelivered, 'Never')}</td>
125 <td>${subscription.deliveryAttemptsSinceSuccess}</td>
126 <td>${dateOrNot(subscription.deliveryNextAttempt, 'Next Publish')}</td>
127 <td>${subscription.id}</td>
133 * Render a row of headers for subscription details.
136 function renderSubscriptionRowHeader() {
138 <th scope="col">Callback URL</th>
139 <th scope="col">Created</th>
140 <th scope="col">Verified</th>
141 <th scope="col">Expires</th>
142 <th scope="col">Using Secret</th>
143 <th scope="col">Signature Type</th>
144 <th scope="col">Remote Address</th>
145 <th scope="col">From</th>
146 <th scope="col">Content Delivered</th>
147 <th scope="col">Content Delivery Failures</th>
148 <th scope="col">Next Delivery</th>
149 <th scope="col">ID</th>
156 * Render the preamble for an HTML page, up through body.
157 * @param {Number} pagePathLevel number of paths below root this page is
158 * @param {String} pageTitle
159 * @param {String[]} headElements
162 function htmlHead(pagePathLevel
, pageTitle
, headElements
= []) {
163 const rootPathPfx
= '../'.repeat(pagePathLevel
);
164 return `<!DOCTYPE html>
167 <meta charset="utf-8">` +
168 headElements
.map((e
) => `${' '.repeat(2)}${e}`).join('\n') + `
169 <title>${pageTitle}</title>
170 <link rel="stylesheet" href="${rootPathPfx}static/theme.css">
177 * Closes remainder of HTML page body.
180 function htmlTail() {
187 * Render a navigation link for the header section.
188 * @param {Object} nav
189 * @param {String} nav.href
190 * @param {String} nav.class
191 * @param {String} nav.text
194 function renderNavLink(nav
) {
196 <a href="${nav.href}"${nav.class ? (' class="' + nav.class + '"') : ''}>${nav.text}</a>
202 * Render the navigation header, and open the main section.
203 * @param {String} pageTitle
204 * @param {Object[]} navLinks
207 function htmlHeader(pageTitle
, navLinks
= []) {
209 <h1>${pageTitle}</h1>
213 ${navLinks.map((l) => renderNavLink(l)).join('\n')}
223 * Close the main section and finish off with boilerplate.
224 * @param {String[]} footerEntries
227 function htmlFooter(footerEntries
= []) {
230 (footerEntries
.length
? `
231 <ol>` + footerEntries
.map((f
) => ` <li>${f}</li>`).join('\n') + `
239 * Render all parts of an HTML page.
240 * @param {Number} pagePathLevel
241 * @param {String} pageTitle
242 * @param {String[]} headElements
243 * @param {Object[]} navLinks
244 * @param {String[]} main
245 * @param {String[]} footerEntries
248 function htmlTemplate(pagePathLevel
, pageTitle
, headElements
= [], navLinks
= [], main
= [], footerEntries
= []) {
250 htmlHead(pagePathLevel
, pageTitle
, headElements
),
251 htmlHeader(pageTitle
, navLinks
),
253 htmlFooter(footerEntries
),
267 renderTopicRowHeader
,
269 renderSubscriptionRowHeader
,
270 renderSubscriptionRow
,