X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=src%2Fmanager.js;h=74730c27be142a5ac33e467fccec182d61bcefde;hb=HEAD;hp=65444a064c33e202afa195208236cab0527f3bab;hpb=8fde3278194ecc46a007984782349e1356435da7;p=websub-hub diff --git a/src/manager.js b/src/manager.js index 65444a0..74730c2 100644 --- a/src/manager.js +++ b/src/manager.js @@ -25,13 +25,6 @@ class Manager { this.db = db; this.options = options; this.communication = new Communication(logger, db, options); - - // Precalculate the invariant root GET metadata. - this.getRootContent = Template.rootHTML(undefined, options); - const now = new Date(); - this.startTimeString = now.toGMTString(); - this.startTimeMs = now.getTime(); - this.getRootETag = common.generateETag(undefined, undefined, this.getRootContent); } @@ -61,16 +54,8 @@ class Manager { const _scope = _fileScope('getRoot'); this.logger.debug(_scope, 'called', { ctx }); - res.setHeader(Enum.Header.LastModified, this.startTimeString); - res.setHeader(Enum.Header.ETag, this.getRootETag); - - if (common.isClientCached(req, this.startTimeMs, this.getRootETag)) { - this.logger.debug(_scope, 'client cached response', { ctx }); - res.statusCode = 304; - res.end(); - return; - } - res.end(this.getRootContent); + const content = Template.rootHTML(ctx, this.options); + res.end(content); this.logger.info(_scope, 'finished', { ctx }); } @@ -558,8 +543,81 @@ class Manager { ctx.count = count.count; }); - res.end(this.infoContent(ctx)); - this.logger.info(_scope, 'finished', { ...ctx }); + 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 + * @param {Number} value + * @returns {String} + */ + 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 ? value : 'no'} update${value === 1 ? '': 's'}`; + } + + + /** + * GET SVG chart of topic update history + * @param {http.ServerResponse} res + * @param {object} ctx + */ + 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 + * @param {URL} topicUrlObj + * @returns {Boolean} + */ + static _profileControlsTopic(profileUrlObj, topicUrlObj) { + const hostnameMatches = profileUrlObj.hostname === topicUrlObj.hostname; + const pathIsPrefix = topicUrlObj.pathname.startsWith(profileUrlObj.pathname); + return hostnameMatches && pathIsPrefix; } @@ -582,12 +640,12 @@ class Manager { const profileUrlObj = new URL(ctx.session.authenticatedProfile); ctx.topics = ctx.topics.filter((topic) => { const topicUrlObj = new URL(topic.url); - return (topicUrlObj.hostname === profileUrlObj.hostname); + return Manager._profileControlsTopic(profileUrlObj, topicUrlObj); }); } res.end(Template.adminOverviewHTML(ctx, this.options)); - this.logger.info(_scope, 'finished', { ...ctx, topics: ctx.topics.length }) + this.logger.info(_scope, 'finished', { ctx, topics: ctx.topics.length }); } @@ -600,25 +658,32 @@ class Manager { const _scope = _fileScope('getTopicDetails'); this.logger.debug(_scope, 'called', { ctx }); + ctx.publishSpan = 60; // FIXME: configurable const topicId = ctx.params.topicId; + let publishHistory; await this.db.context(async (dbCtx) => { ctx.topic = await this.db.topicGetById(dbCtx, topicId); ctx.subscriptions = await this.db.subscriptionsByTopicId(dbCtx, topicId); + publishHistory = await this.db.topicPublishHistory(dbCtx, topicId, ctx.publishSpan); }); - this.logger.debug(_scope, 'got topic details', { topic: ctx.topic, subscriptions: ctx.subscriptions }); + ctx.publishCount = publishHistory.reduce((a, b) => a + b, 0); + ctx.subscriptionsDelivered = ctx.subscriptions.filter((subscription) => { + return subscription.latestContentDelivered >= ctx.topic.contentUpdated; + }).length; + this.logger.debug(_scope, 'got topic details', { topic: ctx.topic, subscriptions: ctx.subscriptions, updates: ctx.publishCount }); // Profile users can only see related topics. if (ctx.session && ctx.session.authenticatedProfile) { const profileUrlObj = new URL(ctx.session.authenticatedProfile); const topicUrlObj = new URL(ctx.topic.url); - if (topicUrlObj.hostname !== profileUrlObj.hostname) { + if (!Manager._profileControlsTopic(profileUrlObj, topicUrlObj)) { ctx.topic = null; ctx.subscriptions = []; } } res.end(Template.adminTopicDetailsHTML(ctx, this.options)); - this.logger.info(_scope, 'finished', { ...ctx, subscriptions: ctx.subscriptions.length, topic: ctx.topic && ctx.topic.id || ctx.topic }); + this.logger.info(_scope, 'finished', { ctx, subscriptions: ctx.subscriptions.length, topic: ctx.topic && ctx.topic.id || ctx.topic }); }