+ const content = this.infoContent(ctx);
+ res.setHeader(Enum.Header.ETag, common.generateETag(undefined, undefined, content));
+ res.setHeader(Enum.Header.CacheControl, 'no-cache');
+ res.end(content);
+ this.logger.info(_scope, 'finished', { ctx });
+ }
+
+
+ /**
+ * label the bars of the topic update history graph
+ * @param {number} index index
+ * @param {number} value value
+ * @returns {string} caption
+ */
+ static _historyBarCaption(index, value) {
+ let when;
+ switch (index) {
+ case 0:
+ when ='today';
+ break;
+ case 1:
+ when = 'yesterday';
+ break;
+ default:
+ when = `${index} days ago`;
+ }
+ return `${when}, ${value || 'no'} update${value === 1 ? '': 's'}`;
+ }
+
+
+ /**
+ * GET SVG chart of topic update history
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
+ */
+ async getHistorySVG(res, ctx) {
+ const _scope = _fileScope('getHistorySVG');
+ this.logger.debug(_scope, 'called', { ctx });
+
+ const days = Math.min(parseInt(ctx.queryParams.days) || this.options.manager.publishHistoryDays, 365);
+ const histOptions = {
+ title: 'Topic Publish History',
+ description: 'Updates per Day',
+ labelZero: '^ Today',
+ labelX: 'Days Ago',
+ maxItems: days,
+ minItems: days,
+ tickEvery: 7,
+ barWidth: 25,
+ barHeight: 40,
+ labelHeight: 12,
+ barCaptionFn: Manager._historyBarCaption,
+ };
+
+ let publishHistory;
+ await this.db.context(async (dbCtx) => {
+ publishHistory = await this.db.topicPublishHistory(dbCtx, ctx.params.topicId, days);
+ });
+
+ res.end(Template.histogramSVG(publishHistory, histOptions));
+ this.logger.info(_scope, 'finished', { ctx });
+ }
+
+
+ /**
+ * Determine if a profile url matches enough of a topic url to describe control over it.
+ * Topic must match hostname and start with the profile's path.
+ * @param {URL} profileUrlObj profile url
+ * @param {URL} topicUrlObj topic url
+ * @returns {boolean} profile is super-url of topic
+ */
+ static _profileControlsTopic(profileUrlObj, topicUrlObj) {
+ const hostnameMatches = profileUrlObj.hostname === topicUrlObj.hostname;
+ const pathIsPrefix = topicUrlObj.pathname.startsWith(profileUrlObj.pathname);
+ return hostnameMatches && pathIsPrefix;