Merge branch 'v1.3-dev' as v1.3.6 v1.3.6
authorJustin Wind <justin.wind+git@gmail.com>
Sat, 19 Mar 2022 22:42:24 +0000 (15:42 -0700)
committerJustin Wind <justin.wind+git@gmail.com>
Sat, 19 Mar 2022 22:42:24 +0000 (15:42 -0700)
40 files changed:
CHANGELOG.md
README.md
config/default.js
documentation/media/postgres-er.svg [new file with mode: 0644]
documentation/media/sqlite-er.svg [new file with mode: 0644]
package-lock.json
package.json
src/db/base.js
src/db/postgres/index.js
src/db/postgres/sql/schema/1.0.0/er.dot [new file with mode: 0644]
src/db/postgres/sql/schema/1.0.1/er.dot [new file with mode: 0644]
src/db/postgres/sql/schema/1.0.2/er.dot [new file with mode: 0644]
src/db/postgres/sql/schema/1.0.3/er.dot [new file with mode: 0644]
src/db/postgres/sql/topic-publish-history.sql [new file with mode: 0644]
src/db/sqlite/index.js
src/db/sqlite/sql/schema/1.0.0/er.dot [new file with mode: 0644]
src/db/sqlite/sql/schema/1.0.1/er.dot [new file with mode: 0644]
src/db/sqlite/sql/schema/1.0.2/er.dot [new file with mode: 0644]
src/db/sqlite/sql/schema/1.0.3/er.dot [new file with mode: 0644]
src/db/sqlite/sql/topic-publish-history.sql [new file with mode: 0644]
src/manager.js
src/service.js
src/template/admin-overview-html.js
src/template/admin-topic-details-html.js
src/template/badge-svg.js
src/template/histogram-svg.js [new file with mode: 0644]
src/template/index.js
src/template/root-html.js
src/template/template-helper.js
static/theme.css
test/src/db/base.js
test/src/db/integration.js
test/src/db/postgres.js
test/src/db/sqlite.js
test/src/manager.js
test/src/service.js
test/src/template/admin-topic-details-html.js
test/src/template/histogram-svg.js [new file with mode: 0644]
test/src/template/template-helper.js
test/stub-db.js

index 99ae8071e4b4e3af6a3ccc1c78a5ee49ee3c911e..f424b04a3951953a7932946e33bcd745302bc2bc 100644 (file)
@@ -2,6 +2,16 @@
 
 Releases and notable changes to this project are documented here.
 
+## [v1.3.6] - TBD
+
+### Added
+
+- Recent history of topic publish updates are now displayed on topic details page.
+
+### Fixed
+
+- Depdency updates.
+
 ## [v1.3.5] - 2022-02-23
 
 ### Added
@@ -124,7 +134,8 @@ Releases and notable changes to this project are documented here.
 
 ---
 
-[Unreleased]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=HEAD;hp=v1.3.5
+[Unreleased]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=HEAD;hp=v1.3.6
+[v1.3.6]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=v1.3.6;hp=v1.3.5
 [v1.3.5]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=v1.3.5;hp=v1.3.4
 [v1.3.4]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=v1.3.4;hp=v1.3.3
 [v1.3.3]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=v1.3.3;hp=v1.3.2
index 138e4583cbd3c6d247d29d8a934beb1692b1823b..5af15f0668c5fca11c22b55e41276860633435ea 100644 (file)
--- a/README.md
+++ b/README.md
@@ -114,6 +114,8 @@ The Hub keeps track of three primary entities:
 
 Any tasks in progress (notably: fetching new topic content, distributing that content to subscribers, or confirming pending verifications) are doled out and managed by a cooperative advisory locking mechanism.  The task queue is wrangled in the database within the `*_in_progress` tables.
 
+![Entity relationship diagram for Postgres engine](./documentation/media/postgres-er.svg)
+
 A Hub node will periodically check for more tasks to perform, executing them up to a set concurrency limit.
 
 ### Quirks
index b23d0f8833919fdd29af7480abd552f9f3b74b30..e4fd7129d046ea9fc3d52a29f1c7b8f256883577 100644 (file)
@@ -53,6 +53,7 @@ const defaultOptions = {
       '<a href="https://git.squeep.com/?p=websub-hub;a=tree">Development Repository</a> / <a href="https://github.com/thylacine/websub-hub/">GitHub mirror</a>',
       '<span class="copyright">&copy;<time datetime="2022">&#8559;&#8559;&#8553;&#8553;&#8544;&#8544;</time></span>',
     ],
+    publishHistoryDays: 60, // Number of days of update history to show on topic details page
     strictSecrets: false, // If true, reject requests with secrets but not over https
     publicHub: true, // Accept publish requests as new topics.
     processImmediately: true, // If true, immediately attempt to process requests when accepted.
diff --git a/documentation/media/postgres-er.svg b/documentation/media/postgres-er.svg
new file mode 100644 (file)
index 0000000..b7db5b4
--- /dev/null
@@ -0,0 +1,250 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.49.0 (20210828.1703)
+ -->
+<!-- Title: WebsubHubERD Pages: 1 -->
+<svg width="922pt" height="1084pt"
+ viewBox="0.00 0.00 922.00 1083.63" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1079.63)">
+<title>WebsubHubERD</title>
+<polygon fill="white" stroke="transparent" points="-4,4 -4,-1079.63 918,-1079.63 918,4 -4,4"/>
+<text text-anchor="middle" x="457" y="-1050.83" font-family="Times,serif" font-size="26.00">Websub Hub Entity&#45;Relations</text>
+<text text-anchor="middle" x="457" y="-1021.83" font-family="Times,serif" font-size="26.00">Postgres</text>
+<text text-anchor="middle" x="457" y="-992.83" font-family="Times,serif" font-size="26.00">Schema 1.0.3</text>
+<!-- topic -->
+<g id="node1" class="node">
+<title>topic</title>
+<polygon fill="lightblue" stroke="transparent" points="0,-665.13 0,-688.13 280,-688.13 280,-665.13 0,-665.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="1,-666.13 1,-687.13 279,-687.13 279,-666.13 1,-666.13"/>
+<text text-anchor="start" x="117" y="-672.93" font-family="Times,serif" font-size="14.00">TOPIC</text>
+<polygon fill="none" stroke="black" points="0,-644.13 0,-665.13 280,-665.13 280,-644.13 0,-644.13"/>
+<text text-anchor="start" x="133" y="-650.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="0,-623.13 0,-644.13 280,-644.13 280,-623.13 0,-623.13"/>
+<text text-anchor="start" x="112.5" y="-629.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="0,-602.13 0,-623.13 280,-623.13 280,-602.13 0,-602.13"/>
+<text text-anchor="start" x="129.5" y="-608.93" font-family="Times,serif" font-size="14.00">url</text>
+<polygon fill="none" stroke="black" points="0,-581.13 0,-602.13 280,-602.13 280,-581.13 0,-581.13"/>
+<text text-anchor="start" x="52" y="-587.93" font-family="Times,serif" font-size="14.00">lease_seconds_preferred</text>
+<polygon fill="none" stroke="black" points="0,-560.13 0,-581.13 280,-581.13 280,-560.13 0,-560.13"/>
+<text text-anchor="start" x="72.5" y="-566.93" font-family="Times,serif" font-size="14.00">lease_seconds_min</text>
+<polygon fill="none" stroke="black" points="0,-539.13 0,-560.13 280,-560.13 280,-539.13 0,-539.13"/>
+<text text-anchor="start" x="71" y="-545.93" font-family="Times,serif" font-size="14.00">lease_seconds_max</text>
+<polygon fill="none" stroke="black" points="0,-518.13 0,-539.13 280,-539.13 280,-518.13 0,-518.13"/>
+<text text-anchor="start" x="54" y="-524.93" font-family="Times,serif" font-size="14.00">publisher_validation_url</text>
+<polygon fill="none" stroke="black" points="0,-497.13 0,-518.13 280,-518.13 280,-497.13 0,-497.13"/>
+<text text-anchor="start" x="54" y="-503.93" font-family="Times,serif" font-size="14.00">content_hash_algorithm</text>
+<polygon fill="none" stroke="black" points="0,-476.13 0,-497.13 280,-497.13 280,-476.13 0,-476.13"/>
+<text text-anchor="start" x="109" y="-482.93" font-family="Times,serif" font-size="14.00">is_active</text>
+<polygon fill="none" stroke="black" points="0,-455.13 0,-476.13 280,-476.13 280,-455.13 0,-455.13"/>
+<text text-anchor="start" x="103.5" y="-461.93" font-family="Times,serif" font-size="14.00">is_deleted</text>
+<polygon fill="none" stroke="black" points="0,-434.13 0,-455.13 280,-455.13 280,-434.13 0,-434.13"/>
+<text text-anchor="start" x="97" y="-440.93" font-family="Times,serif" font-size="14.00">last_publish</text>
+<polygon fill="none" stroke="black" points="0,-413.13 0,-434.13 280,-434.13 280,-413.13 0,-413.13"/>
+<text text-anchor="start" x="40.5" y="-419.93" font-family="Times,serif" font-size="14.00">content_fetch_next_attempt</text>
+<polygon fill="none" stroke="black" points="0,-392.13 0,-413.13 280,-413.13 280,-392.13 0,-392.13"/>
+<text text-anchor="start" x="3" y="-398.93" font-family="Times,serif" font-size="14.00">content_fetch_attempts_since_success</text>
+<polygon fill="none" stroke="black" points="0,-371.13 0,-392.13 280,-392.13 280,-371.13 0,-371.13"/>
+<text text-anchor="start" x="80" y="-377.93" font-family="Times,serif" font-size="14.00">content_updated</text>
+<polygon fill="none" stroke="black" points="0,-350.13 0,-371.13 280,-371.13 280,-350.13 0,-350.13"/>
+<text text-anchor="start" x="112.5" y="-356.93" font-family="Times,serif" font-size="14.00">content</text>
+<polygon fill="none" stroke="black" points="0,-329.13 0,-350.13 280,-350.13 280,-329.13 0,-329.13"/>
+<text text-anchor="start" x="92.5" y="-335.93" font-family="Times,serif" font-size="14.00">content_hash</text>
+<polygon fill="none" stroke="black" points="0,-308.13 0,-329.13 280,-329.13 280,-308.13 0,-308.13"/>
+<text text-anchor="start" x="93.5" y="-314.93" font-family="Times,serif" font-size="14.00">content_type</text>
+</g>
+<!-- topic_fetch_in_progress -->
+<g id="node2" class="node">
+<title>topic_fetch_in_progress</title>
+<polygon fill="lightblue" stroke="transparent" points="323.5,-957.13 323.5,-980.13 550.5,-980.13 550.5,-957.13 323.5,-957.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="324.5,-958.13 324.5,-979.13 549.5,-979.13 549.5,-958.13 324.5,-958.13"/>
+<text text-anchor="start" x="327.5" y="-964.93" font-family="Times,serif" font-size="14.00">TOPIC_FETCH_IN_PROGRESS</text>
+<polygon fill="none" stroke="black" points="323.5,-936.13 323.5,-957.13 550.5,-957.13 550.5,-936.13 323.5,-936.13"/>
+<text text-anchor="start" x="430" y="-942.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="323.5,-915.13 323.5,-936.13 550.5,-936.13 550.5,-915.13 323.5,-915.13"/>
+<text text-anchor="start" x="406" y="-921.93" font-family="Times,serif" font-size="14.00">claimant</text>
+<polygon fill="none" stroke="black" points="323.5,-894.13 323.5,-915.13 550.5,-915.13 550.5,-894.13 323.5,-894.13"/>
+<text text-anchor="start" x="409" y="-900.93" font-family="Times,serif" font-size="14.00">claimed</text>
+<polygon fill="none" stroke="black" points="323.5,-873.13 323.5,-894.13 550.5,-894.13 550.5,-873.13 323.5,-873.13"/>
+<text text-anchor="start" x="388" y="-879.93" font-family="Times,serif" font-size="14.00">claim_expires</text>
+</g>
+<!-- topic&#45;&gt;topic_fetch_in_progress -->
+<g id="edge1" class="edge">
+<title>topic:pk_id&#45;&gt;topic_fetch_in_progress:fk_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C341.86,-655.13 266.47,-914.99 312.79,-944.43"/>
+<polygon fill="black" stroke="black" points="312.87,-944.45 321.29,-951.47 317.68,-945.79 322.5,-947.13 322.5,-947.13 322.5,-947.13 317.68,-945.79 323.71,-942.8 312.87,-944.45 312.87,-944.45"/>
+</g>
+<!-- topic_content_history -->
+<g id="node3" class="node">
+<title>topic_content_history</title>
+<polygon fill="lightblue" stroke="transparent" points="332.5,-832.13 332.5,-855.13 541.5,-855.13 541.5,-832.13 332.5,-832.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="333.5,-833.13 333.5,-854.13 540.5,-854.13 540.5,-833.13 333.5,-833.13"/>
+<text text-anchor="start" x="336.5" y="-839.93" font-family="Times,serif" font-size="14.00">TOPIC_CONTENT_HISTORY</text>
+<polygon fill="none" stroke="black" points="332.5,-811.13 332.5,-832.13 541.5,-832.13 541.5,-811.13 332.5,-811.13"/>
+<text text-anchor="start" x="409" y="-817.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="332.5,-790.13 332.5,-811.13 541.5,-811.13 541.5,-790.13 332.5,-790.13"/>
+<text text-anchor="start" x="377" y="-796.93" font-family="Times,serif" font-size="14.00">content_updated</text>
+<polygon fill="none" stroke="black" points="332.5,-769.13 332.5,-790.13 541.5,-790.13 541.5,-769.13 332.5,-769.13"/>
+<text text-anchor="start" x="392.5" y="-775.93" font-family="Times,serif" font-size="14.00">content_size</text>
+<polygon fill="none" stroke="black" points="332.5,-748.13 332.5,-769.13 541.5,-769.13 541.5,-748.13 332.5,-748.13"/>
+<text text-anchor="start" x="389.5" y="-754.93" font-family="Times,serif" font-size="14.00">content_hash</text>
+</g>
+<!-- topic&#45;&gt;topic_content_history -->
+<g id="edge2" class="edge">
+<title>topic:pk_id&#45;&gt;topic_content_history:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C353.88,-655.13 263.54,-806.22 321.38,-820.98"/>
+<polygon fill="black" stroke="black" points="321.56,-821 330.99,-826.6 326.53,-821.56 331.5,-822.13 331.5,-822.13 331.5,-822.13 326.53,-821.56 332.01,-817.66 321.56,-821 321.56,-821"/>
+</g>
+<!-- subscription -->
+<g id="node4" class="node">
+<title>subscription</title>
+<polygon fill="lightblue" stroke="transparent" points="316.5,-707.13 316.5,-730.13 557.5,-730.13 557.5,-707.13 316.5,-707.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="317.5,-708.13 317.5,-729.13 556.5,-729.13 556.5,-708.13 317.5,-708.13"/>
+<text text-anchor="start" x="379" y="-714.93" font-family="Times,serif" font-size="14.00">SUBSCRIPTION</text>
+<polygon fill="none" stroke="black" points="316.5,-686.13 316.5,-707.13 557.5,-707.13 557.5,-686.13 316.5,-686.13"/>
+<text text-anchor="start" x="430" y="-692.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="316.5,-665.13 316.5,-686.13 557.5,-686.13 557.5,-665.13 316.5,-665.13"/>
+<text text-anchor="start" x="409.5" y="-671.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="316.5,-644.13 316.5,-665.13 557.5,-665.13 557.5,-644.13 316.5,-644.13"/>
+<text text-anchor="start" x="409" y="-650.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="316.5,-623.13 316.5,-644.13 557.5,-644.13 557.5,-623.13 316.5,-623.13"/>
+<text text-anchor="start" x="408" y="-629.93" font-family="Times,serif" font-size="14.00">callback</text>
+<polygon fill="none" stroke="black" points="316.5,-602.13 316.5,-623.13 557.5,-623.13 557.5,-602.13 316.5,-602.13"/>
+<text text-anchor="start" x="409.5" y="-608.93" font-family="Times,serif" font-size="14.00">verified</text>
+<polygon fill="none" stroke="black" points="316.5,-581.13 316.5,-602.13 557.5,-602.13 557.5,-581.13 316.5,-581.13"/>
+<text text-anchor="start" x="410.5" y="-587.93" font-family="Times,serif" font-size="14.00">expires</text>
+<polygon fill="none" stroke="black" points="316.5,-560.13 316.5,-581.13 557.5,-581.13 557.5,-560.13 316.5,-560.13"/>
+<text text-anchor="start" x="414.5" y="-566.93" font-family="Times,serif" font-size="14.00">secret</text>
+<polygon fill="none" stroke="black" points="316.5,-539.13 316.5,-560.13 557.5,-560.13 557.5,-539.13 316.5,-539.13"/>
+<text text-anchor="start" x="364.5" y="-545.93" font-family="Times,serif" font-size="14.00">signature_algorithm</text>
+<polygon fill="none" stroke="black" points="316.5,-518.13 316.5,-539.13 557.5,-539.13 557.5,-518.13 316.5,-518.13"/>
+<text text-anchor="start" x="373" y="-524.93" font-family="Times,serif" font-size="14.00">http_remote_addr</text>
+<polygon fill="none" stroke="black" points="316.5,-497.13 316.5,-518.13 557.5,-518.13 557.5,-497.13 316.5,-497.13"/>
+<text text-anchor="start" x="401.5" y="-503.93" font-family="Times,serif" font-size="14.00">http_from</text>
+<polygon fill="none" stroke="black" points="316.5,-476.13 316.5,-497.13 557.5,-497.13 557.5,-476.13 316.5,-476.13"/>
+<text text-anchor="start" x="373" y="-482.93" font-family="Times,serif" font-size="14.00">content_delivered</text>
+<polygon fill="none" stroke="black" points="316.5,-455.13 316.5,-476.13 557.5,-476.13 557.5,-455.13 316.5,-455.13"/>
+<text text-anchor="start" x="349.5" y="-461.93" font-family="Times,serif" font-size="14.00">latest_content_delivered</text>
+<polygon fill="none" stroke="black" points="316.5,-434.13 316.5,-455.13 557.5,-455.13 557.5,-434.13 316.5,-434.13"/>
+<text text-anchor="start" x="319.5" y="-440.93" font-family="Times,serif" font-size="14.00">delivery_attempts_since_success</text>
+<polygon fill="none" stroke="black" points="316.5,-413.13 316.5,-434.13 557.5,-434.13 557.5,-413.13 316.5,-413.13"/>
+<text text-anchor="start" x="357" y="-419.93" font-family="Times,serif" font-size="14.00">delivery_next_attempt</text>
+</g>
+<!-- topic&#45;&gt;subscription -->
+<g id="edge3" class="edge">
+<title>topic:pk_id&#45;&gt;subscription:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C292,-655.13 297.25,-655.13 305.88,-655.13"/>
+<polygon fill="black" stroke="black" points="306,-655.13 316,-659.63 311,-655.13 316,-655.13 316,-655.13 316,-655.13 311,-655.13 316,-650.63 306,-655.13 306,-655.13"/>
+</g>
+<!-- verification -->
+<g id="node6" class="node">
+<title>verification</title>
+<polygon fill="lightblue" stroke="transparent" points="353.5,-372.13 353.5,-395.13 519.5,-395.13 519.5,-372.13 353.5,-372.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="354.5,-373.13 354.5,-394.13 518.5,-394.13 518.5,-373.13 354.5,-373.13"/>
+<text text-anchor="start" x="382" y="-379.93" font-family="Times,serif" font-size="14.00">VERIFICATION</text>
+<polygon fill="none" stroke="black" points="353.5,-351.13 353.5,-372.13 519.5,-372.13 519.5,-351.13 353.5,-351.13"/>
+<text text-anchor="start" x="429.5" y="-357.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="353.5,-330.13 353.5,-351.13 519.5,-351.13 519.5,-330.13 353.5,-330.13"/>
+<text text-anchor="start" x="409" y="-336.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="353.5,-309.13 353.5,-330.13 519.5,-330.13 519.5,-309.13 353.5,-309.13"/>
+<text text-anchor="start" x="408.5" y="-315.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="353.5,-288.13 353.5,-309.13 519.5,-309.13 519.5,-288.13 353.5,-288.13"/>
+<text text-anchor="start" x="407.5" y="-294.93" font-family="Times,serif" font-size="14.00">callback</text>
+<polygon fill="none" stroke="black" points="353.5,-267.13 353.5,-288.13 519.5,-288.13 519.5,-267.13 353.5,-267.13"/>
+<text text-anchor="start" x="414" y="-273.93" font-family="Times,serif" font-size="14.00">secret</text>
+<polygon fill="none" stroke="black" points="353.5,-246.13 353.5,-267.13 519.5,-267.13 519.5,-246.13 353.5,-246.13"/>
+<text text-anchor="start" x="364" y="-252.93" font-family="Times,serif" font-size="14.00">signature_algorithm</text>
+<polygon fill="none" stroke="black" points="353.5,-225.13 353.5,-246.13 519.5,-246.13 519.5,-225.13 353.5,-225.13"/>
+<text text-anchor="start" x="372.5" y="-231.93" font-family="Times,serif" font-size="14.00">http_remote_addr</text>
+<polygon fill="none" stroke="black" points="353.5,-204.13 353.5,-225.13 519.5,-225.13 519.5,-204.13 353.5,-204.13"/>
+<text text-anchor="start" x="401" y="-210.93" font-family="Times,serif" font-size="14.00">http_from</text>
+<polygon fill="none" stroke="black" points="353.5,-183.13 353.5,-204.13 519.5,-204.13 519.5,-183.13 353.5,-183.13"/>
+<text text-anchor="start" x="416.5" y="-189.93" font-family="Times,serif" font-size="14.00">mode</text>
+<polygon fill="none" stroke="black" points="353.5,-162.13 353.5,-183.13 519.5,-183.13 519.5,-162.13 353.5,-162.13"/>
+<text text-anchor="start" x="412" y="-168.93" font-family="Times,serif" font-size="14.00">reason</text>
+<polygon fill="none" stroke="black" points="353.5,-141.13 353.5,-162.13 519.5,-162.13 519.5,-141.13 353.5,-141.13"/>
+<text text-anchor="start" x="386" y="-147.93" font-family="Times,serif" font-size="14.00">lease_seconds</text>
+<polygon fill="none" stroke="black" points="353.5,-120.13 353.5,-141.13 519.5,-141.13 519.5,-120.13 353.5,-120.13"/>
+<text text-anchor="start" x="356.5" y="-126.93" font-family="Times,serif" font-size="14.00">is_publisher_validated</text>
+<polygon fill="none" stroke="black" points="353.5,-99.13 353.5,-120.13 519.5,-120.13 519.5,-99.13 353.5,-99.13"/>
+<text text-anchor="start" x="398.5" y="-105.93" font-family="Times,serif" font-size="14.00">request_id</text>
+<polygon fill="none" stroke="black" points="353.5,-78.13 353.5,-99.13 519.5,-99.13 519.5,-78.13 353.5,-78.13"/>
+<text text-anchor="start" x="404" y="-84.93" font-family="Times,serif" font-size="14.00">attempts</text>
+<polygon fill="none" stroke="black" points="353.5,-57.13 353.5,-78.13 519.5,-78.13 519.5,-57.13 353.5,-57.13"/>
+<text text-anchor="start" x="388.5" y="-63.93" font-family="Times,serif" font-size="14.00">next_attempt</text>
+</g>
+<!-- topic&#45;&gt;verification -->
+<g id="edge5" class="edge">
+<title>topic:pk_id&#45;&gt;verification:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C336.13,-655.13 300.8,-459.16 316,-405.13 326.09,-369.27 314.1,-328.6 342.37,-321.29"/>
+<polygon fill="black" stroke="black" points="342.56,-321.26 353.01,-324.6 347.53,-320.7 352.5,-320.13 352.5,-320.13 352.5,-320.13 347.53,-320.7 351.99,-315.66 342.56,-321.26 342.56,-321.26"/>
+</g>
+<!-- verification_in_progress -->
+<g id="node7" class="node">
+<title>verification_in_progress</title>
+<polygon fill="lightblue" stroke="transparent" points="638.5,-216.13 638.5,-239.13 869.5,-239.13 869.5,-216.13 638.5,-216.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="639.5,-217.13 639.5,-238.13 868.5,-238.13 868.5,-217.13 639.5,-217.13"/>
+<text text-anchor="start" x="642.5" y="-223.93" font-family="Times,serif" font-size="14.00">VERIFICATION_IN_PROGRESS</text>
+<polygon fill="none" stroke="black" points="638.5,-195.13 638.5,-216.13 869.5,-216.13 869.5,-195.13 638.5,-195.13"/>
+<text text-anchor="start" x="747" y="-201.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="638.5,-174.13 638.5,-195.13 869.5,-195.13 869.5,-174.13 638.5,-174.13"/>
+<text text-anchor="start" x="726" y="-180.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="638.5,-153.13 638.5,-174.13 869.5,-174.13 869.5,-153.13 638.5,-153.13"/>
+<text text-anchor="start" x="725" y="-159.93" font-family="Times,serif" font-size="14.00">callback</text>
+<polygon fill="none" stroke="black" points="638.5,-132.13 638.5,-153.13 869.5,-153.13 869.5,-132.13 638.5,-132.13"/>
+<text text-anchor="start" x="723" y="-138.93" font-family="Times,serif" font-size="14.00">claimant</text>
+<polygon fill="none" stroke="black" points="638.5,-111.13 638.5,-132.13 869.5,-132.13 869.5,-111.13 638.5,-111.13"/>
+<text text-anchor="start" x="726" y="-117.93" font-family="Times,serif" font-size="14.00">claimed</text>
+<polygon fill="none" stroke="black" points="638.5,-90.13 638.5,-111.13 869.5,-111.13 869.5,-90.13 638.5,-90.13"/>
+<text text-anchor="start" x="705" y="-96.93" font-family="Times,serif" font-size="14.00">claim_expires</text>
+</g>
+<!-- topic&#45;&gt;verification_in_progress -->
+<g id="edge7" class="edge">
+<title>topic:pk_id&#45;&gt;verification_in_progress:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C347.56,-655.13 266.83,-94.47 316,-48.13 393.95,25.33 464.03,5.07 557,-48.13 615.06,-81.36 571.99,-174.57 627.31,-184.31"/>
+<polygon fill="black" stroke="black" points="627.53,-184.32 637.14,-189.62 632.52,-184.73 637.5,-185.13 637.5,-185.13 637.5,-185.13 632.52,-184.73 637.86,-180.65 627.53,-184.32 627.53,-184.32"/>
+</g>
+<!-- subscription_delivery_in_progress -->
+<g id="node5" class="node">
+<title>subscription_delivery_in_progress</title>
+<polygon fill="lightblue" stroke="transparent" points="593.5,-707.13 593.5,-730.13 914.5,-730.13 914.5,-707.13 593.5,-707.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="594.5,-708.13 594.5,-729.13 913.5,-729.13 913.5,-708.13 594.5,-708.13"/>
+<text text-anchor="start" x="597.5" y="-714.93" font-family="Times,serif" font-size="14.00">SUBSCRIPTION_DELIVERY_IN_PROGRESS</text>
+<polygon fill="none" stroke="black" points="593.5,-686.13 593.5,-707.13 914.5,-707.13 914.5,-686.13 593.5,-686.13"/>
+<text text-anchor="start" x="747" y="-692.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="593.5,-665.13 593.5,-686.13 914.5,-686.13 914.5,-665.13 593.5,-665.13"/>
+<text text-anchor="start" x="723" y="-671.93" font-family="Times,serif" font-size="14.00">claimant</text>
+<polygon fill="none" stroke="black" points="593.5,-644.13 593.5,-665.13 914.5,-665.13 914.5,-644.13 593.5,-644.13"/>
+<text text-anchor="start" x="726" y="-650.93" font-family="Times,serif" font-size="14.00">claimed</text>
+<polygon fill="none" stroke="black" points="593.5,-623.13 593.5,-644.13 914.5,-644.13 914.5,-623.13 593.5,-623.13"/>
+<text text-anchor="start" x="705" y="-629.93" font-family="Times,serif" font-size="14.00">claim_expires</text>
+</g>
+<!-- subscription&#45;&gt;subscription_delivery_in_progress -->
+<g id="edge4" class="edge">
+<title>subscription:pk_id&#45;&gt;subscription_delivery_in_progress:fk_id</title>
+<path fill="none" stroke="black" d="M557,-697.13C569,-697.13 574.25,-697.13 582.88,-697.13"/>
+<polygon fill="black" stroke="black" points="583,-697.13 593,-701.63 588,-697.13 593,-697.13 593,-697.13 593,-697.13 588,-697.13 593,-692.63 583,-697.13 583,-697.13"/>
+</g>
+<!-- verification&#45;&gt;verification_in_progress -->
+<g id="edge6" class="edge">
+<title>verification:pk_id&#45;&gt;verification_in_progress:fk_id</title>
+<path fill="none" stroke="black" d="M520.5,-362.13C603.44,-362.13 555.41,-219.25 627.1,-206.97"/>
+<polygon fill="black" stroke="black" points="627.53,-206.94 637.86,-210.62 632.52,-206.53 637.5,-206.13 637.5,-206.13 637.5,-206.13 632.52,-206.53 637.14,-201.65 627.53,-206.94 627.53,-206.94"/>
+</g>
+<!-- authentication -->
+<g id="node8" class="node">
+<title>authentication</title>
+<polygon fill="lightblue" stroke="transparent" points="67,-790.13 67,-813.13 213,-813.13 213,-790.13 67,-790.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="68,-791.13 68,-812.13 212,-812.13 212,-791.13 68,-791.13"/>
+<text text-anchor="start" x="71" y="-797.93" font-family="Times,serif" font-size="14.00">AUTHENTICATION</text>
+<polygon fill="none" stroke="black" points="67,-769.13 67,-790.13 213,-790.13 213,-769.13 67,-769.13"/>
+<text text-anchor="start" x="112.5" y="-775.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="67,-748.13 67,-769.13 213,-769.13 213,-748.13 67,-748.13"/>
+<text text-anchor="start" x="73.5" y="-754.93" font-family="Times,serif" font-size="14.00">last_authenticated</text>
+<polygon fill="none" stroke="black" points="67,-727.13 67,-748.13 213,-748.13 213,-727.13 67,-727.13"/>
+<text text-anchor="start" x="106.5" y="-733.93" font-family="Times,serif" font-size="14.00">identifier</text>
+<polygon fill="none" stroke="black" points="67,-706.13 67,-727.13 213,-727.13 213,-706.13 67,-706.13"/>
+<text text-anchor="start" x="103.5" y="-712.93" font-family="Times,serif" font-size="14.00">credential</text>
+</g>
+</g>
+</svg>
diff --git a/documentation/media/sqlite-er.svg b/documentation/media/sqlite-er.svg
new file mode 100644 (file)
index 0000000..e4a9c87
--- /dev/null
@@ -0,0 +1,250 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.49.0 (20210828.1703)
+ -->
+<!-- Title: WebsubHubERD Pages: 1 -->
+<svg width="922pt" height="1084pt"
+ viewBox="0.00 0.00 922.00 1083.63" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1079.63)">
+<title>WebsubHubERD</title>
+<polygon fill="white" stroke="transparent" points="-4,4 -4,-1079.63 918,-1079.63 918,4 -4,4"/>
+<text text-anchor="middle" x="457" y="-1050.83" font-family="Times,serif" font-size="26.00">Websub Hub Entity&#45;Relations</text>
+<text text-anchor="middle" x="457" y="-1021.83" font-family="Times,serif" font-size="26.00">SQLite</text>
+<text text-anchor="middle" x="457" y="-992.83" font-family="Times,serif" font-size="26.00">Schema 1.0.3</text>
+<!-- topic -->
+<g id="node1" class="node">
+<title>topic</title>
+<polygon fill="lightblue" stroke="transparent" points="0,-665.13 0,-688.13 280,-688.13 280,-665.13 0,-665.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="1,-666.13 1,-687.13 279,-687.13 279,-666.13 1,-666.13"/>
+<text text-anchor="start" x="117" y="-672.93" font-family="Times,serif" font-size="14.00">TOPIC</text>
+<polygon fill="none" stroke="black" points="0,-644.13 0,-665.13 280,-665.13 280,-644.13 0,-644.13"/>
+<text text-anchor="start" x="133" y="-650.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="0,-623.13 0,-644.13 280,-644.13 280,-623.13 0,-623.13"/>
+<text text-anchor="start" x="112.5" y="-629.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="0,-602.13 0,-623.13 280,-623.13 280,-602.13 0,-602.13"/>
+<text text-anchor="start" x="129.5" y="-608.93" font-family="Times,serif" font-size="14.00">url</text>
+<polygon fill="none" stroke="black" points="0,-581.13 0,-602.13 280,-602.13 280,-581.13 0,-581.13"/>
+<text text-anchor="start" x="52" y="-587.93" font-family="Times,serif" font-size="14.00">lease_seconds_preferred</text>
+<polygon fill="none" stroke="black" points="0,-560.13 0,-581.13 280,-581.13 280,-560.13 0,-560.13"/>
+<text text-anchor="start" x="72.5" y="-566.93" font-family="Times,serif" font-size="14.00">lease_seconds_min</text>
+<polygon fill="none" stroke="black" points="0,-539.13 0,-560.13 280,-560.13 280,-539.13 0,-539.13"/>
+<text text-anchor="start" x="71" y="-545.93" font-family="Times,serif" font-size="14.00">lease_seconds_max</text>
+<polygon fill="none" stroke="black" points="0,-518.13 0,-539.13 280,-539.13 280,-518.13 0,-518.13"/>
+<text text-anchor="start" x="54" y="-524.93" font-family="Times,serif" font-size="14.00">publisher_validation_url</text>
+<polygon fill="none" stroke="black" points="0,-497.13 0,-518.13 280,-518.13 280,-497.13 0,-497.13"/>
+<text text-anchor="start" x="54" y="-503.93" font-family="Times,serif" font-size="14.00">content_hash_algorithm</text>
+<polygon fill="none" stroke="black" points="0,-476.13 0,-497.13 280,-497.13 280,-476.13 0,-476.13"/>
+<text text-anchor="start" x="109" y="-482.93" font-family="Times,serif" font-size="14.00">is_active</text>
+<polygon fill="none" stroke="black" points="0,-455.13 0,-476.13 280,-476.13 280,-455.13 0,-455.13"/>
+<text text-anchor="start" x="103.5" y="-461.93" font-family="Times,serif" font-size="14.00">is_deleted</text>
+<polygon fill="none" stroke="black" points="0,-434.13 0,-455.13 280,-455.13 280,-434.13 0,-434.13"/>
+<text text-anchor="start" x="97" y="-440.93" font-family="Times,serif" font-size="14.00">last_publish</text>
+<polygon fill="none" stroke="black" points="0,-413.13 0,-434.13 280,-434.13 280,-413.13 0,-413.13"/>
+<text text-anchor="start" x="40.5" y="-419.93" font-family="Times,serif" font-size="14.00">content_fetch_next_attempt</text>
+<polygon fill="none" stroke="black" points="0,-392.13 0,-413.13 280,-413.13 280,-392.13 0,-392.13"/>
+<text text-anchor="start" x="3" y="-398.93" font-family="Times,serif" font-size="14.00">content_fetch_attempts_since_success</text>
+<polygon fill="none" stroke="black" points="0,-371.13 0,-392.13 280,-392.13 280,-371.13 0,-371.13"/>
+<text text-anchor="start" x="80" y="-377.93" font-family="Times,serif" font-size="14.00">content_updated</text>
+<polygon fill="none" stroke="black" points="0,-350.13 0,-371.13 280,-371.13 280,-350.13 0,-350.13"/>
+<text text-anchor="start" x="112.5" y="-356.93" font-family="Times,serif" font-size="14.00">content</text>
+<polygon fill="none" stroke="black" points="0,-329.13 0,-350.13 280,-350.13 280,-329.13 0,-329.13"/>
+<text text-anchor="start" x="92.5" y="-335.93" font-family="Times,serif" font-size="14.00">content_hash</text>
+<polygon fill="none" stroke="black" points="0,-308.13 0,-329.13 280,-329.13 280,-308.13 0,-308.13"/>
+<text text-anchor="start" x="93.5" y="-314.93" font-family="Times,serif" font-size="14.00">content_type</text>
+</g>
+<!-- topic_fetch_in_progress -->
+<g id="node2" class="node">
+<title>topic_fetch_in_progress</title>
+<polygon fill="lightblue" stroke="transparent" points="323.5,-957.13 323.5,-980.13 550.5,-980.13 550.5,-957.13 323.5,-957.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="324.5,-958.13 324.5,-979.13 549.5,-979.13 549.5,-958.13 324.5,-958.13"/>
+<text text-anchor="start" x="327.5" y="-964.93" font-family="Times,serif" font-size="14.00">TOPIC_FETCH_IN_PROGRESS</text>
+<polygon fill="none" stroke="black" points="323.5,-936.13 323.5,-957.13 550.5,-957.13 550.5,-936.13 323.5,-936.13"/>
+<text text-anchor="start" x="430" y="-942.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="323.5,-915.13 323.5,-936.13 550.5,-936.13 550.5,-915.13 323.5,-915.13"/>
+<text text-anchor="start" x="406" y="-921.93" font-family="Times,serif" font-size="14.00">claimant</text>
+<polygon fill="none" stroke="black" points="323.5,-894.13 323.5,-915.13 550.5,-915.13 550.5,-894.13 323.5,-894.13"/>
+<text text-anchor="start" x="409" y="-900.93" font-family="Times,serif" font-size="14.00">claimed</text>
+<polygon fill="none" stroke="black" points="323.5,-873.13 323.5,-894.13 550.5,-894.13 550.5,-873.13 323.5,-873.13"/>
+<text text-anchor="start" x="388" y="-879.93" font-family="Times,serif" font-size="14.00">claim_expires</text>
+</g>
+<!-- topic&#45;&gt;topic_fetch_in_progress -->
+<g id="edge1" class="edge">
+<title>topic:pk_id&#45;&gt;topic_fetch_in_progress:fk_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C341.86,-655.13 266.47,-914.99 312.79,-944.43"/>
+<polygon fill="black" stroke="black" points="312.87,-944.45 321.29,-951.47 317.68,-945.79 322.5,-947.13 322.5,-947.13 322.5,-947.13 317.68,-945.79 323.71,-942.8 312.87,-944.45 312.87,-944.45"/>
+</g>
+<!-- topic_content_history -->
+<g id="node3" class="node">
+<title>topic_content_history</title>
+<polygon fill="lightblue" stroke="transparent" points="332.5,-832.13 332.5,-855.13 541.5,-855.13 541.5,-832.13 332.5,-832.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="333.5,-833.13 333.5,-854.13 540.5,-854.13 540.5,-833.13 333.5,-833.13"/>
+<text text-anchor="start" x="336.5" y="-839.93" font-family="Times,serif" font-size="14.00">TOPIC_CONTENT_HISTORY</text>
+<polygon fill="none" stroke="black" points="332.5,-811.13 332.5,-832.13 541.5,-832.13 541.5,-811.13 332.5,-811.13"/>
+<text text-anchor="start" x="409" y="-817.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="332.5,-790.13 332.5,-811.13 541.5,-811.13 541.5,-790.13 332.5,-790.13"/>
+<text text-anchor="start" x="377" y="-796.93" font-family="Times,serif" font-size="14.00">content_updated</text>
+<polygon fill="none" stroke="black" points="332.5,-769.13 332.5,-790.13 541.5,-790.13 541.5,-769.13 332.5,-769.13"/>
+<text text-anchor="start" x="392.5" y="-775.93" font-family="Times,serif" font-size="14.00">content_size</text>
+<polygon fill="none" stroke="black" points="332.5,-748.13 332.5,-769.13 541.5,-769.13 541.5,-748.13 332.5,-748.13"/>
+<text text-anchor="start" x="389.5" y="-754.93" font-family="Times,serif" font-size="14.00">content_hash</text>
+</g>
+<!-- topic&#45;&gt;topic_content_history -->
+<g id="edge2" class="edge">
+<title>topic:pk_id&#45;&gt;topic_content_history:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C353.88,-655.13 263.54,-806.22 321.38,-820.98"/>
+<polygon fill="black" stroke="black" points="321.56,-821 330.99,-826.6 326.53,-821.56 331.5,-822.13 331.5,-822.13 331.5,-822.13 326.53,-821.56 332.01,-817.66 321.56,-821 321.56,-821"/>
+</g>
+<!-- subscription -->
+<g id="node4" class="node">
+<title>subscription</title>
+<polygon fill="lightblue" stroke="transparent" points="316.5,-707.13 316.5,-730.13 557.5,-730.13 557.5,-707.13 316.5,-707.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="317.5,-708.13 317.5,-729.13 556.5,-729.13 556.5,-708.13 317.5,-708.13"/>
+<text text-anchor="start" x="379" y="-714.93" font-family="Times,serif" font-size="14.00">SUBSCRIPTION</text>
+<polygon fill="none" stroke="black" points="316.5,-686.13 316.5,-707.13 557.5,-707.13 557.5,-686.13 316.5,-686.13"/>
+<text text-anchor="start" x="430" y="-692.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="316.5,-665.13 316.5,-686.13 557.5,-686.13 557.5,-665.13 316.5,-665.13"/>
+<text text-anchor="start" x="409.5" y="-671.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="316.5,-644.13 316.5,-665.13 557.5,-665.13 557.5,-644.13 316.5,-644.13"/>
+<text text-anchor="start" x="409" y="-650.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="316.5,-623.13 316.5,-644.13 557.5,-644.13 557.5,-623.13 316.5,-623.13"/>
+<text text-anchor="start" x="408" y="-629.93" font-family="Times,serif" font-size="14.00">callback</text>
+<polygon fill="none" stroke="black" points="316.5,-602.13 316.5,-623.13 557.5,-623.13 557.5,-602.13 316.5,-602.13"/>
+<text text-anchor="start" x="409.5" y="-608.93" font-family="Times,serif" font-size="14.00">verified</text>
+<polygon fill="none" stroke="black" points="316.5,-581.13 316.5,-602.13 557.5,-602.13 557.5,-581.13 316.5,-581.13"/>
+<text text-anchor="start" x="410.5" y="-587.93" font-family="Times,serif" font-size="14.00">expires</text>
+<polygon fill="none" stroke="black" points="316.5,-560.13 316.5,-581.13 557.5,-581.13 557.5,-560.13 316.5,-560.13"/>
+<text text-anchor="start" x="414.5" y="-566.93" font-family="Times,serif" font-size="14.00">secret</text>
+<polygon fill="none" stroke="black" points="316.5,-539.13 316.5,-560.13 557.5,-560.13 557.5,-539.13 316.5,-539.13"/>
+<text text-anchor="start" x="364.5" y="-545.93" font-family="Times,serif" font-size="14.00">signature_algorithm</text>
+<polygon fill="none" stroke="black" points="316.5,-518.13 316.5,-539.13 557.5,-539.13 557.5,-518.13 316.5,-518.13"/>
+<text text-anchor="start" x="373" y="-524.93" font-family="Times,serif" font-size="14.00">http_remote_addr</text>
+<polygon fill="none" stroke="black" points="316.5,-497.13 316.5,-518.13 557.5,-518.13 557.5,-497.13 316.5,-497.13"/>
+<text text-anchor="start" x="401.5" y="-503.93" font-family="Times,serif" font-size="14.00">http_from</text>
+<polygon fill="none" stroke="black" points="316.5,-476.13 316.5,-497.13 557.5,-497.13 557.5,-476.13 316.5,-476.13"/>
+<text text-anchor="start" x="373" y="-482.93" font-family="Times,serif" font-size="14.00">content_delivered</text>
+<polygon fill="none" stroke="black" points="316.5,-455.13 316.5,-476.13 557.5,-476.13 557.5,-455.13 316.5,-455.13"/>
+<text text-anchor="start" x="349.5" y="-461.93" font-family="Times,serif" font-size="14.00">latest_content_delivered</text>
+<polygon fill="none" stroke="black" points="316.5,-434.13 316.5,-455.13 557.5,-455.13 557.5,-434.13 316.5,-434.13"/>
+<text text-anchor="start" x="319.5" y="-440.93" font-family="Times,serif" font-size="14.00">delivery_attempts_since_success</text>
+<polygon fill="none" stroke="black" points="316.5,-413.13 316.5,-434.13 557.5,-434.13 557.5,-413.13 316.5,-413.13"/>
+<text text-anchor="start" x="357" y="-419.93" font-family="Times,serif" font-size="14.00">delivery_next_attempt</text>
+</g>
+<!-- topic&#45;&gt;subscription -->
+<g id="edge3" class="edge">
+<title>topic:pk_id&#45;&gt;subscription:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C292,-655.13 297.25,-655.13 305.88,-655.13"/>
+<polygon fill="black" stroke="black" points="306,-655.13 316,-659.63 311,-655.13 316,-655.13 316,-655.13 316,-655.13 311,-655.13 316,-650.63 306,-655.13 306,-655.13"/>
+</g>
+<!-- verification -->
+<g id="node6" class="node">
+<title>verification</title>
+<polygon fill="lightblue" stroke="transparent" points="353.5,-372.13 353.5,-395.13 519.5,-395.13 519.5,-372.13 353.5,-372.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="354.5,-373.13 354.5,-394.13 518.5,-394.13 518.5,-373.13 354.5,-373.13"/>
+<text text-anchor="start" x="382" y="-379.93" font-family="Times,serif" font-size="14.00">VERIFICATION</text>
+<polygon fill="none" stroke="black" points="353.5,-351.13 353.5,-372.13 519.5,-372.13 519.5,-351.13 353.5,-351.13"/>
+<text text-anchor="start" x="429.5" y="-357.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="353.5,-330.13 353.5,-351.13 519.5,-351.13 519.5,-330.13 353.5,-330.13"/>
+<text text-anchor="start" x="409" y="-336.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="353.5,-309.13 353.5,-330.13 519.5,-330.13 519.5,-309.13 353.5,-309.13"/>
+<text text-anchor="start" x="408.5" y="-315.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="353.5,-288.13 353.5,-309.13 519.5,-309.13 519.5,-288.13 353.5,-288.13"/>
+<text text-anchor="start" x="407.5" y="-294.93" font-family="Times,serif" font-size="14.00">callback</text>
+<polygon fill="none" stroke="black" points="353.5,-267.13 353.5,-288.13 519.5,-288.13 519.5,-267.13 353.5,-267.13"/>
+<text text-anchor="start" x="414" y="-273.93" font-family="Times,serif" font-size="14.00">secret</text>
+<polygon fill="none" stroke="black" points="353.5,-246.13 353.5,-267.13 519.5,-267.13 519.5,-246.13 353.5,-246.13"/>
+<text text-anchor="start" x="364" y="-252.93" font-family="Times,serif" font-size="14.00">signature_algorithm</text>
+<polygon fill="none" stroke="black" points="353.5,-225.13 353.5,-246.13 519.5,-246.13 519.5,-225.13 353.5,-225.13"/>
+<text text-anchor="start" x="372.5" y="-231.93" font-family="Times,serif" font-size="14.00">http_remote_addr</text>
+<polygon fill="none" stroke="black" points="353.5,-204.13 353.5,-225.13 519.5,-225.13 519.5,-204.13 353.5,-204.13"/>
+<text text-anchor="start" x="401" y="-210.93" font-family="Times,serif" font-size="14.00">http_from</text>
+<polygon fill="none" stroke="black" points="353.5,-183.13 353.5,-204.13 519.5,-204.13 519.5,-183.13 353.5,-183.13"/>
+<text text-anchor="start" x="416.5" y="-189.93" font-family="Times,serif" font-size="14.00">mode</text>
+<polygon fill="none" stroke="black" points="353.5,-162.13 353.5,-183.13 519.5,-183.13 519.5,-162.13 353.5,-162.13"/>
+<text text-anchor="start" x="412" y="-168.93" font-family="Times,serif" font-size="14.00">reason</text>
+<polygon fill="none" stroke="black" points="353.5,-141.13 353.5,-162.13 519.5,-162.13 519.5,-141.13 353.5,-141.13"/>
+<text text-anchor="start" x="386" y="-147.93" font-family="Times,serif" font-size="14.00">lease_seconds</text>
+<polygon fill="none" stroke="black" points="353.5,-120.13 353.5,-141.13 519.5,-141.13 519.5,-120.13 353.5,-120.13"/>
+<text text-anchor="start" x="356.5" y="-126.93" font-family="Times,serif" font-size="14.00">is_publisher_validated</text>
+<polygon fill="none" stroke="black" points="353.5,-99.13 353.5,-120.13 519.5,-120.13 519.5,-99.13 353.5,-99.13"/>
+<text text-anchor="start" x="398.5" y="-105.93" font-family="Times,serif" font-size="14.00">request_id</text>
+<polygon fill="none" stroke="black" points="353.5,-78.13 353.5,-99.13 519.5,-99.13 519.5,-78.13 353.5,-78.13"/>
+<text text-anchor="start" x="404" y="-84.93" font-family="Times,serif" font-size="14.00">attempts</text>
+<polygon fill="none" stroke="black" points="353.5,-57.13 353.5,-78.13 519.5,-78.13 519.5,-57.13 353.5,-57.13"/>
+<text text-anchor="start" x="388.5" y="-63.93" font-family="Times,serif" font-size="14.00">next_attempt</text>
+</g>
+<!-- topic&#45;&gt;verification -->
+<g id="edge5" class="edge">
+<title>topic:pk_id&#45;&gt;verification:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C336.13,-655.13 300.8,-459.16 316,-405.13 326.09,-369.27 314.1,-328.6 342.37,-321.29"/>
+<polygon fill="black" stroke="black" points="342.56,-321.26 353.01,-324.6 347.53,-320.7 352.5,-320.13 352.5,-320.13 352.5,-320.13 347.53,-320.7 351.99,-315.66 342.56,-321.26 342.56,-321.26"/>
+</g>
+<!-- verification_in_progress -->
+<g id="node7" class="node">
+<title>verification_in_progress</title>
+<polygon fill="lightblue" stroke="transparent" points="638.5,-216.13 638.5,-239.13 869.5,-239.13 869.5,-216.13 638.5,-216.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="639.5,-217.13 639.5,-238.13 868.5,-238.13 868.5,-217.13 639.5,-217.13"/>
+<text text-anchor="start" x="642.5" y="-223.93" font-family="Times,serif" font-size="14.00">VERIFICATION_IN_PROGRESS</text>
+<polygon fill="none" stroke="black" points="638.5,-195.13 638.5,-216.13 869.5,-216.13 869.5,-195.13 638.5,-195.13"/>
+<text text-anchor="start" x="747" y="-201.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="638.5,-174.13 638.5,-195.13 869.5,-195.13 869.5,-174.13 638.5,-174.13"/>
+<text text-anchor="start" x="726" y="-180.93" font-family="Times,serif" font-size="14.00">topic_id</text>
+<polygon fill="none" stroke="black" points="638.5,-153.13 638.5,-174.13 869.5,-174.13 869.5,-153.13 638.5,-153.13"/>
+<text text-anchor="start" x="725" y="-159.93" font-family="Times,serif" font-size="14.00">callback</text>
+<polygon fill="none" stroke="black" points="638.5,-132.13 638.5,-153.13 869.5,-153.13 869.5,-132.13 638.5,-132.13"/>
+<text text-anchor="start" x="723" y="-138.93" font-family="Times,serif" font-size="14.00">claimant</text>
+<polygon fill="none" stroke="black" points="638.5,-111.13 638.5,-132.13 869.5,-132.13 869.5,-111.13 638.5,-111.13"/>
+<text text-anchor="start" x="726" y="-117.93" font-family="Times,serif" font-size="14.00">claimed</text>
+<polygon fill="none" stroke="black" points="638.5,-90.13 638.5,-111.13 869.5,-111.13 869.5,-90.13 638.5,-90.13"/>
+<text text-anchor="start" x="705" y="-96.93" font-family="Times,serif" font-size="14.00">claim_expires</text>
+</g>
+<!-- topic&#45;&gt;verification_in_progress -->
+<g id="edge7" class="edge">
+<title>topic:pk_id&#45;&gt;verification_in_progress:fk_topic_id</title>
+<path fill="none" stroke="black" d="M280,-655.13C347.56,-655.13 266.83,-94.47 316,-48.13 393.95,25.33 464.03,5.07 557,-48.13 615.06,-81.36 571.99,-174.57 627.31,-184.31"/>
+<polygon fill="black" stroke="black" points="627.53,-184.32 637.14,-189.62 632.52,-184.73 637.5,-185.13 637.5,-185.13 637.5,-185.13 632.52,-184.73 637.86,-180.65 627.53,-184.32 627.53,-184.32"/>
+</g>
+<!-- subscription_delivery_in_progress -->
+<g id="node5" class="node">
+<title>subscription_delivery_in_progress</title>
+<polygon fill="lightblue" stroke="transparent" points="593.5,-707.13 593.5,-730.13 914.5,-730.13 914.5,-707.13 593.5,-707.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="594.5,-708.13 594.5,-729.13 913.5,-729.13 913.5,-708.13 594.5,-708.13"/>
+<text text-anchor="start" x="597.5" y="-714.93" font-family="Times,serif" font-size="14.00">SUBSCRIPTION_DELIVERY_IN_PROGRESS</text>
+<polygon fill="none" stroke="black" points="593.5,-686.13 593.5,-707.13 914.5,-707.13 914.5,-686.13 593.5,-686.13"/>
+<text text-anchor="start" x="747" y="-692.93" font-family="Times,serif" font-size="14.00">id</text>
+<polygon fill="none" stroke="black" points="593.5,-665.13 593.5,-686.13 914.5,-686.13 914.5,-665.13 593.5,-665.13"/>
+<text text-anchor="start" x="723" y="-671.93" font-family="Times,serif" font-size="14.00">claimant</text>
+<polygon fill="none" stroke="black" points="593.5,-644.13 593.5,-665.13 914.5,-665.13 914.5,-644.13 593.5,-644.13"/>
+<text text-anchor="start" x="726" y="-650.93" font-family="Times,serif" font-size="14.00">claimed</text>
+<polygon fill="none" stroke="black" points="593.5,-623.13 593.5,-644.13 914.5,-644.13 914.5,-623.13 593.5,-623.13"/>
+<text text-anchor="start" x="705" y="-629.93" font-family="Times,serif" font-size="14.00">claim_expires</text>
+</g>
+<!-- subscription&#45;&gt;subscription_delivery_in_progress -->
+<g id="edge4" class="edge">
+<title>subscription:pk_id&#45;&gt;subscription_delivery_in_progress:fk_id</title>
+<path fill="none" stroke="black" d="M557,-697.13C569,-697.13 574.25,-697.13 582.88,-697.13"/>
+<polygon fill="black" stroke="black" points="583,-697.13 593,-701.63 588,-697.13 593,-697.13 593,-697.13 593,-697.13 588,-697.13 593,-692.63 583,-697.13 583,-697.13"/>
+</g>
+<!-- verification&#45;&gt;verification_in_progress -->
+<g id="edge6" class="edge">
+<title>verification:pk_id&#45;&gt;verification_in_progress:fk_id</title>
+<path fill="none" stroke="black" d="M520.5,-362.13C603.44,-362.13 555.41,-219.25 627.1,-206.97"/>
+<polygon fill="black" stroke="black" points="627.53,-206.94 637.86,-210.62 632.52,-206.53 637.5,-206.13 637.5,-206.13 637.5,-206.13 632.52,-206.53 637.14,-201.65 627.53,-206.94 627.53,-206.94"/>
+</g>
+<!-- authentication -->
+<g id="node8" class="node">
+<title>authentication</title>
+<polygon fill="lightblue" stroke="transparent" points="67,-790.13 67,-813.13 213,-813.13 213,-790.13 67,-790.13"/>
+<polygon fill="none" stroke="black" stroke-width="2" points="68,-791.13 68,-812.13 212,-812.13 212,-791.13 68,-791.13"/>
+<text text-anchor="start" x="71" y="-797.93" font-family="Times,serif" font-size="14.00">AUTHENTICATION</text>
+<polygon fill="none" stroke="black" points="67,-769.13 67,-790.13 213,-790.13 213,-769.13 67,-769.13"/>
+<text text-anchor="start" x="112.5" y="-775.93" font-family="Times,serif" font-size="14.00">created</text>
+<polygon fill="none" stroke="black" points="67,-748.13 67,-769.13 213,-769.13 213,-748.13 67,-748.13"/>
+<text text-anchor="start" x="73.5" y="-754.93" font-family="Times,serif" font-size="14.00">last_authenticated</text>
+<polygon fill="none" stroke="black" points="67,-727.13 67,-748.13 213,-748.13 213,-727.13 67,-727.13"/>
+<text text-anchor="start" x="106.5" y="-733.93" font-family="Times,serif" font-size="14.00">identifier</text>
+<polygon fill="none" stroke="black" points="67,-706.13 67,-727.13 213,-727.13 213,-706.13 67,-706.13"/>
+<text text-anchor="start" x="103.5" y="-712.93" font-family="Times,serif" font-size="14.00">credential</text>
+</g>
+</g>
+</svg>
index a5089d4b14cbfa378944b67c05f3522efee83cfa..249034b4951170f3a59901b3d90f179c41939856 100644 (file)
@@ -1,6 +1,6 @@
 {
   "name": "@squeep/websub-hub",
-  "version": "1.3.5",
+  "version": "1.3.6",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
       }
     },
     "@eslint/eslintrc": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.1.0.tgz",
-      "integrity": "sha512-C1DfL7XX4nPqGd6jcP01W9pVM1HYCuUkFk1432D7F0v3JSlUIeOYn9oCoi3eoLZ+iwBSb29BMFxxny0YrrEZqg==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz",
+      "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==",
       "dev": true,
       "requires": {
         "ajv": "^6.12.4",
         "debug": "^4.3.2",
         "espree": "^9.3.1",
         "globals": "^13.9.0",
-        "ignore": "^4.0.6",
+        "ignore": "^5.2.0",
         "import-fresh": "^3.2.1",
         "js-yaml": "^4.1.0",
         "minimatch": "^3.0.4",
           "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
           "dev": true
         },
-        "ignore": {
-          "version": "4.0.6",
-          "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
-          "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
-          "dev": true
-        },
         "js-yaml": {
           "version": "4.1.0",
           "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
       }
     },
     "@humanwhocodes/config-array": {
-      "version": "0.9.3",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.3.tgz",
-      "integrity": "sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ==",
+      "version": "0.9.5",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
+      "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==",
       "dev": true,
       "requires": {
         "@humanwhocodes/object-schema": "^1.2.1",
       "dev": true
     },
     "@squeep/api-dingus": {
-      "version": "git+https://git.squeep.com/squeep-api-dingus/#0ebfbe2181ce363e13d8b6fd10c9703897d5306f",
-      "from": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.4",
+      "version": "git+https://git.squeep.com/squeep-api-dingus/#12568946a94e853c3c16974d57dd34de1ad3877c",
+      "from": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.5",
       "requires": {
         "mime-db": "^1.51.0",
         "uuid": "^8.3.2"
       }
     },
     "@squeep/authentication-module": {
-      "version": "git+https://git.squeep.com/squeep-authentication-module/#92658b114da01ab5537c53dee7ff5ad6385fe179",
-      "from": "git+https://git.squeep.com/squeep-authentication-module/#v1.2.0",
+      "version": "git+https://git.squeep.com/squeep-authentication-module/#017c7190ecb48afa9b9cd164c8d9cd3b62fb29df",
+      "from": "git+https://git.squeep.com/squeep-authentication-module/#v1.2.1",
       "requires": {
-        "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.4",
-        "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.2",
-        "@squeep/indieauth-helper": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.1.0",
-        "@squeep/mystery-box": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.3",
+        "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.5",
+        "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.4",
+        "@squeep/indieauth-helper": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.1.1",
+        "@squeep/mystery-box": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.4",
         "argon2": "^0.28.4",
         "node-linux-pam": "^0.2.1"
       }
     },
     "@squeep/html-template-helper": {
-      "version": "git+https://git.squeep.com/squeep-html-template-helper#5506e8de3b9c93e2ec2d37d71134eee8ee5fd27c",
-      "from": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.2"
+      "version": "git+https://git.squeep.com/squeep-html-template-helper#f8d8a0142eaf713a9258c4f5738cdcb6b6e5c7bd",
+      "from": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.4"
     },
     "@squeep/indieauth-helper": {
-      "version": "git+https://git.squeep.com/squeep-indieauth-helper/#7ece3489799b5349e22e95e3bd9fe7a30a985ebf",
-      "from": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.1.0",
+      "version": "git+https://git.squeep.com/squeep-indieauth-helper/#f26fc3563484621523dd4d8a8efdc0e020ab6943",
+      "from": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.1.1",
       "requires": {
-        "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.3",
-        "axios": "^0.26.0",
+        "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.4",
+        "axios": "^0.26.1",
         "iconv": "^3.0.1",
         "microformats-parser": "^1.4.1"
       }
     },
     "@squeep/mystery-box": {
-      "version": "git+https://git.squeep.com/squeep-mystery-box/#22091e8c1fd175c17031c6237e6a166271edffd1",
-      "from": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.3"
+      "version": "git+https://git.squeep.com/squeep-mystery-box/#c47d2327a3a642792e9521677cd5f551e9aa7bfb",
+      "from": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.4"
     },
     "@squeep/web-linking": {
-      "version": "git+https://git.squeep.com/squeep-web-linking/#a09659d26192d35e11461a7776f4f76c2e211576",
-      "from": "git+https://git.squeep.com/squeep-web-linking/#v1.0.3"
+      "version": "git+https://git.squeep.com/squeep-web-linking/#9a786b67a16416996df95b63c1a295dae98cf663",
+      "from": "git+https://git.squeep.com/squeep-web-linking/#v1.0.4"
     },
     "@ungap/promise-all-settled": {
       "version": "1.1.2",
       }
     },
     "argon2": {
-      "version": "0.28.4",
-      "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.28.4.tgz",
-      "integrity": "sha512-WsfqiDp/tf5+eieLc1+S7RtO7Y3cAiZQ1F6GIaskENoJy/6xuCN5WGBIc8dG7QVPDavy6jUSads8zwZTtrHVag==",
+      "version": "0.28.5",
+      "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.28.5.tgz",
+      "integrity": "sha512-kGFCctzc3VWmR1aCOYjNgvoTmVF5uVBUtWlXCKKO54d1K+31zRz45KAcDIqMo2746ozv/52d25nfEekitaXP0w==",
       "optional": true,
       "requires": {
         "@mapbox/node-pre-gyp": "^1.0.8",
         "@phc/format": "^1.0.0",
-        "node-addon-api": "^4.3.0",
-        "opencollective-postinstall": "^2.0.3"
+        "node-addon-api": "^4.3.0"
       }
     },
     "argparse": {
       "optional": true
     },
     "axios": {
-      "version": "0.26.0",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
-      "integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
+      "version": "0.26.1",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
+      "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
       "requires": {
         "follow-redirects": "^1.14.8"
       }
       "dev": true
     },
     "caniuse-lite": {
-      "version": "1.0.30001243",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001243.tgz",
-      "integrity": "sha512-vNxw9mkTBtkmLFnJRv/2rhs1yufpDfCkBZexG3Y0xdOH2Z/eE/85E4Dl5j1YUN34nZVsSp6vVRFQRrez9wJMRA==",
+      "version": "1.0.30001315",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001315.tgz",
+      "integrity": "sha512-5v7LFQU4Sb/qvkz7JcZkvtSH1Ko+1x2kgo3ocdBeMGZSOFpuE1kkm0kpTwLtWeFrw5qw08ulLxJjVIXIS8MkiQ==",
       "dev": true
     },
     "chalk": {
       "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
     },
     "eslint": {
-      "version": "8.9.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.9.0.tgz",
-      "integrity": "sha512-PB09IGwv4F4b0/atrbcMFboF/giawbBLVC7fyDamk5Wtey4Jh2K+rYaBhCAbUyEI4QzB1ly09Uglc9iCtFaG2Q==",
+      "version": "8.11.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.11.0.tgz",
+      "integrity": "sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA==",
       "dev": true,
       "requires": {
-        "@eslint/eslintrc": "^1.1.0",
+        "@eslint/eslintrc": "^1.2.1",
         "@humanwhocodes/config-array": "^0.9.2",
         "ajv": "^6.10.0",
         "chalk": "^4.0.0",
       "dev": true
     },
     "follow-redirects": {
-      "version": "1.14.8",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
-      "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
+      "version": "1.14.9",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
+      "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
     },
     "foreground-child": {
       "version": "2.0.0",
       }
     },
     "mime-db": {
-      "version": "1.51.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
-      "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
     },
     "mimic-response": {
       "version": "3.1.0",
       "optional": true
     },
     "mocha": {
-      "version": "9.2.1",
-      "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.1.tgz",
-      "integrity": "sha512-T7uscqjJVS46Pq1XDXyo9Uvey9gd3huT/DD9cYBb4K2Xc/vbKRPUWK067bxDQRK0yIz6Jxk73IrnimvASzBNAQ==",
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz",
+      "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==",
       "dev": true,
       "requires": {
         "@ungap/promise-all-settled": "1.1.2",
         "he": "1.2.0",
         "js-yaml": "4.1.0",
         "log-symbols": "4.1.0",
-        "minimatch": "3.0.4",
+        "minimatch": "4.2.1",
         "ms": "2.1.3",
-        "nanoid": "3.2.0",
+        "nanoid": "3.3.1",
         "serialize-javascript": "6.0.0",
         "strip-json-comments": "3.1.1",
         "supports-color": "8.1.1",
             "minimatch": "^3.0.4",
             "once": "^1.3.0",
             "path-is-absolute": "^1.0.0"
+          },
+          "dependencies": {
+            "minimatch": {
+              "version": "3.1.2",
+              "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+              "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+              "dev": true,
+              "requires": {
+                "brace-expansion": "^1.1.7"
+              }
+            }
           }
         },
         "has-flag": {
             "argparse": "^2.0.1"
           }
         },
+        "minimatch": {
+          "version": "4.2.1",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz",
+          "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
         "ms": {
           "version": "2.1.3",
           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
     "nanoid": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
-      "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==",
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
+      "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==",
       "dev": true
     },
     "napi-build-utils": {
         "wrappy": "1"
       }
     },
-    "opencollective-postinstall": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
-      "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
-      "optional": true
-    },
     "optionator": {
       "version": "0.9.1",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
index 71843a3b4c1a6baa1744b30ae73a153a96181866..188f2d6d09df9c2168b7e0da4be1a271f24fb4f2 100644 (file)
@@ -1,6 +1,6 @@
 {
   "name": "@squeep/websub-hub",
-  "version": "1.3.5",
+  "version": "1.3.6",
   "description": "A WebSub Hub server implementation.",
   "main": "server.js",
   "scripts": {
     "coverage-check"
   ],
   "dependencies": {
-    "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.4",
-    "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.2.0",
-    "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.2",
-    "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.3",
-    "axios": "^0.26.0",
+    "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.5",
+    "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.2.1",
+    "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.4",
+    "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.4",
+    "axios": "^0.26.1",
     "feedparser": "^2.2.10",
     "htmlparser2": "^7.2.0",
     "iconv": "^3.0.1"
     "pg-promise": "^10.11.1"
   },
   "devDependencies": {
-    "eslint": "^8.9.0",
+    "eslint": "^8.11.0",
     "eslint-plugin-node": "^11.1.0",
     "eslint-plugin-security": "^1.4.0",
     "eslint-plugin-sonarjs": "^0.12.0",
     "html-minifier-lint": "^2.0.0",
-    "mocha": "^9.2.1",
+    "mocha": "^9.2.2",
     "mocha-steps": "^1.3.0",
     "nyc": "^15.1.0",
     "pre-commit": "^1.2.2",
index de0cd44a1b9c5a77eb0e8fcb22771ffbf7c488c1..3346f92d421ac7d65932a6e21df5cddc2ddf635d 100644 (file)
@@ -585,6 +585,18 @@ class Database {
   }
 
 
+  /**
+   * Return an array of the counts of the last #days of topic updates.
+   * @param {*} dbCtx
+   * @param {*} topicId
+   * @param {Number} days
+   * @returns {Number[]}
+   */
+  async topicPublishHistory(dbCtx, topicId, days) {
+    this._notImplemented('topicPublishHistory', arguments);
+  }
+
+
   /**
    * Create or update the basic parameters of a topic.
    * @param {*} dbCtx 
@@ -598,7 +610,7 @@ class Database {
   /**
    * Updates a topic's content data and content update timestamp.
    * @param {Object} data
-   * @param {Integer} data.topicId
+   * @param {*} data.topicId
    * @param {String} data.content
    * @param {String} data.contentHash
    * @param {String=} data.contentType
index d950d4a8488924ea2999a9c03b03d93f38ab4591..fc4ec8daa1b229ec353ebfeff8cf5ac3d917a6d8 100644 (file)
@@ -856,6 +856,18 @@ class DatabasePostgres extends Database {
   }
 
 
+  async topicPublishHistory(dbCtx, topicId, days) {
+    const _scope = _fileScope('topicPublishHistory');
+    this.logger.debug(_scope, 'called', { topicId, days });
+
+    const events = await dbCtx.manyOrNone(this.statement.topicPublishHistory, { topicIds: [topicId], daysAgo: days });
+    const history = Array.from({ length: days }, () => 0);
+    events.forEach(({ daysAgo, contentUpdates }) => history[daysAgo] = Number(contentUpdates));
+
+    return history;
+  }
+
+
   async topicSet(dbCtx, data) {
     const _scope = _fileScope('topicSet');
     this.logger.debug(_scope, 'called', data);
diff --git a/src/db/postgres/sql/schema/1.0.0/er.dot b/src/db/postgres/sql/schema/1.0.0/er.dot
new file mode 100644 (file)
index 0000000..a70f150
--- /dev/null
@@ -0,0 +1,125 @@
+digraph WebsubHubERD {
+       graph[
+               rankdir=LR,
+               overlap=false,
+               splines=true,
+               label="Websub Hub Entity-Relations\nPostgres\nSchema 1.0.0",
+               labelloc="t",
+               fontsize=26,
+       ];
+       // layout=neato;
+       node[shape=plain];
+       edge[arrowhead=crow];
+
+       topic [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">url</td></tr>
+               <tr><td port="">lease_seconds_preferred</td></tr>
+               <tr><td port="">lease_seconds_min</td></tr>
+               <tr><td port="">lease_seconds_max</td></tr>
+               <tr><td port="">publisher_validation_url</td></tr>
+               <tr><td port="">content_hash_algorithm</td></tr>
+               <tr><td port="">is_active</td></tr>
+               <tr><td port="">is_deleted</td></tr>
+               <tr><td port="">last_publish</td></tr>
+               <tr><td port="">content_fetch_next_attempt</td></tr>
+               <tr><td port="">content_fetch_attempts_since_success</td></tr>
+               <tr><td port="">content_updated</td></tr>
+               <tr><td port="">content</td></tr>
+               <tr><td port="">content_hash</td></tr>
+               <tr><td port="">content_type</td></tr>
+       </table>
+       >];
+
+       topic_fetch_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+       subscription [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">verified</td></tr>
+               <tr><td port="">expires</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">content_delivered</td></tr>
+               <tr><td port="">delivery_attempts_since_success</td></tr>
+               <tr><td port="">delivery_next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> subscription:fk_topic_id;
+
+       subscription_delivery_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+       verification [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">mode</td></tr>
+               <tr><td port="">reason</td></tr>
+               <tr><td port="">lease_seconds</td></tr>
+               <tr><td port="">is_publisher_validated</td></tr>
+               <tr><td port="">request_id</td></tr>
+               <tr><td port="">attempts</td></tr>
+               <tr><td port="">next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> verification:fk_topic_id;
+
+       verification_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       verification:pk_id -> verification_in_progress:fk_id;
+       topic:pk_id -> verification_in_progress:fk_topic_id;
+
+       authentication [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">last_authenticated</td></tr>
+               <tr><td port="">identifier</td></tr>
+               <tr><td port="">credential</td></tr>
+       </table>
+       >];
+
+}
\ No newline at end of file
diff --git a/src/db/postgres/sql/schema/1.0.1/er.dot b/src/db/postgres/sql/schema/1.0.1/er.dot
new file mode 100644 (file)
index 0000000..53b63f2
--- /dev/null
@@ -0,0 +1,125 @@
+digraph WebsubHubERD {
+       graph[
+               rankdir=LR,
+               overlap=false,
+               splines=true,
+               label="Websub Hub Entity-Relations\nPostgres\nSchema 1.0.1",
+               labelloc="t",
+               fontsize=26,
+       ];
+       // layout=neato;
+       node[shape=plain];
+       edge[arrowhead=crow];
+
+       topic [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">url</td></tr>
+               <tr><td port="">lease_seconds_preferred</td></tr>
+               <tr><td port="">lease_seconds_min</td></tr>
+               <tr><td port="">lease_seconds_max</td></tr>
+               <tr><td port="">publisher_validation_url</td></tr>
+               <tr><td port="">content_hash_algorithm</td></tr>
+               <tr><td port="">is_active</td></tr>
+               <tr><td port="">is_deleted</td></tr>
+               <tr><td port="">last_publish</td></tr>
+               <tr><td port="">content_fetch_next_attempt</td></tr>
+               <tr><td port="">content_fetch_attempts_since_success</td></tr>
+               <tr><td port="">content_updated</td></tr>
+               <tr><td port="">content</td></tr>
+               <tr><td port="">content_hash</td></tr>
+               <tr><td port="">content_type</td></tr>
+       </table>
+       >];
+
+       topic_fetch_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+       subscription [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">verified</td></tr>
+               <tr><td port="">expires</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">content_delivered</td></tr>
+               <tr><td port="">delivery_attempts_since_success</td></tr>
+               <tr><td port="">delivery_next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> subscription:fk_topic_id;
+
+       subscription_delivery_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+       verification [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">mode</td></tr>
+               <tr><td port="">reason</td></tr>
+               <tr><td port="">lease_seconds</td></tr>
+               <tr><td port="">is_publisher_validated</td></tr>
+               <tr><td port="">request_id</td></tr>
+               <tr><td port="">attempts</td></tr>
+               <tr><td port="">next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> verification:fk_topic_id;
+
+       verification_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       verification:pk_id -> verification_in_progress:fk_id;
+       topic:pk_id -> verification_in_progress:fk_topic_id;
+
+       authentication [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">last_authenticated</td></tr>
+               <tr><td port="">identifier</td></tr>
+               <tr><td port="">credential</td></tr>
+       </table>
+       >];
+
+}
\ No newline at end of file
diff --git a/src/db/postgres/sql/schema/1.0.2/er.dot b/src/db/postgres/sql/schema/1.0.2/er.dot
new file mode 100644 (file)
index 0000000..c9bd7ed
--- /dev/null
@@ -0,0 +1,126 @@
+digraph WebsubHubERD {
+       graph[
+               rankdir=LR,
+               overlap=false,
+               splines=true,
+               label="Websub Hub Entity-Relations\nPostgres\nSchema 1.0.2",
+               labelloc="t",
+               fontsize=26,
+       ];
+       // layout=neato;
+       node[shape=plain];
+       edge[arrowhead=crow];
+
+       topic [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">url</td></tr>
+               <tr><td port="">lease_seconds_preferred</td></tr>
+               <tr><td port="">lease_seconds_min</td></tr>
+               <tr><td port="">lease_seconds_max</td></tr>
+               <tr><td port="">publisher_validation_url</td></tr>
+               <tr><td port="">content_hash_algorithm</td></tr>
+               <tr><td port="">is_active</td></tr>
+               <tr><td port="">is_deleted</td></tr>
+               <tr><td port="">last_publish</td></tr>
+               <tr><td port="">content_fetch_next_attempt</td></tr>
+               <tr><td port="">content_fetch_attempts_since_success</td></tr>
+               <tr><td port="">content_updated</td></tr>
+               <tr><td port="">content</td></tr>
+               <tr><td port="">content_hash</td></tr>
+               <tr><td port="">content_type</td></tr>
+       </table>
+       >];
+
+       topic_fetch_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+       subscription [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">verified</td></tr>
+               <tr><td port="">expires</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">content_delivered</td></tr>
+               <tr><td port="">latest_content_delivered</td></tr>
+               <tr><td port="">delivery_attempts_since_success</td></tr>
+               <tr><td port="">delivery_next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> subscription:fk_topic_id;
+
+       subscription_delivery_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+       verification [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">mode</td></tr>
+               <tr><td port="">reason</td></tr>
+               <tr><td port="">lease_seconds</td></tr>
+               <tr><td port="">is_publisher_validated</td></tr>
+               <tr><td port="">request_id</td></tr>
+               <tr><td port="">attempts</td></tr>
+               <tr><td port="">next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> verification:fk_topic_id;
+
+       verification_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       verification:pk_id -> verification_in_progress:fk_id;
+       topic:pk_id -> verification_in_progress:fk_topic_id;
+
+       authentication [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">last_authenticated</td></tr>
+               <tr><td port="">identifier</td></tr>
+               <tr><td port="">credential</td></tr>
+       </table>
+       >];
+
+}
\ No newline at end of file
diff --git a/src/db/postgres/sql/schema/1.0.3/er.dot b/src/db/postgres/sql/schema/1.0.3/er.dot
new file mode 100644 (file)
index 0000000..eae5a5f
--- /dev/null
@@ -0,0 +1,137 @@
+digraph WebsubHubERD {
+       graph[
+               rankdir=LR,
+               overlap=false,
+               splines=true,
+               label="Websub Hub Entity-Relations\nPostgres\nSchema 1.0.3",
+               labelloc="t",
+               fontsize=26,
+       ];
+       // layout=neato;
+       node[shape=plain];
+       edge[arrowhead=crow];
+
+       topic [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">url</td></tr>
+               <tr><td port="">lease_seconds_preferred</td></tr>
+               <tr><td port="">lease_seconds_min</td></tr>
+               <tr><td port="">lease_seconds_max</td></tr>
+               <tr><td port="">publisher_validation_url</td></tr>
+               <tr><td port="">content_hash_algorithm</td></tr>
+               <tr><td port="">is_active</td></tr>
+               <tr><td port="">is_deleted</td></tr>
+               <tr><td port="">last_publish</td></tr>
+               <tr><td port="">content_fetch_next_attempt</td></tr>
+               <tr><td port="">content_fetch_attempts_since_success</td></tr>
+               <tr><td port="">content_updated</td></tr>
+               <tr><td port="">content</td></tr>
+               <tr><td port="">content_hash</td></tr>
+               <tr><td port="">content_type</td></tr>
+       </table>
+       >];
+
+       topic_fetch_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+       topic_content_history [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC_CONTENT_HISTORY</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">content_updated</td></tr>
+               <tr><td port="">content_size</td></tr>
+               <tr><td port="">content_hash</td></tr>
+       </table>
+       >];
+       topic:pk_id -> topic_content_history:fk_topic_id;
+
+       subscription [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">verified</td></tr>
+               <tr><td port="">expires</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">content_delivered</td></tr>
+               <tr><td port="">latest_content_delivered</td></tr>
+               <tr><td port="">delivery_attempts_since_success</td></tr>
+               <tr><td port="">delivery_next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> subscription:fk_topic_id;
+
+       subscription_delivery_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+       verification [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">mode</td></tr>
+               <tr><td port="">reason</td></tr>
+               <tr><td port="">lease_seconds</td></tr>
+               <tr><td port="">is_publisher_validated</td></tr>
+               <tr><td port="">request_id</td></tr>
+               <tr><td port="">attempts</td></tr>
+               <tr><td port="">next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> verification:fk_topic_id;
+
+       verification_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       verification:pk_id -> verification_in_progress:fk_id;
+       topic:pk_id -> verification_in_progress:fk_topic_id;
+
+       authentication [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">last_authenticated</td></tr>
+               <tr><td port="">identifier</td></tr>
+               <tr><td port="">credential</td></tr>
+       </table>
+       >];
+
+}
\ No newline at end of file
diff --git a/src/db/postgres/sql/topic-publish-history.sql b/src/db/postgres/sql/topic-publish-history.sql
new file mode 100644 (file)
index 0000000..e4010e8
--- /dev/null
@@ -0,0 +1,12 @@
+--
+SELECT
+       EXTRACT(DAY FROM now() - content_updated::date) AS days_ago,
+       count(*) AS content_updates
+FROM topic_content_history
+WHERE
+       (now() - content_updated::date) < ($(daysAgo) * INTERVAL '1 day')
+AND
+       ($(topicIds) IS NULL OR topic_id = ANY($(topicIds)::uuid[]))
+GROUP BY content_updated::date
+ORDER BY content_updated::date
+;
index b8826a4f15ee424113e72fd790b41b8212bf3cf8..31471bafb850c9d761ffc19931968658eb093258 100644 (file)
@@ -822,6 +822,18 @@ class DatabaseSQLite extends Database {
   }
 
 
+  topicPublishHistory(dbCtx, topicId, days) {
+    const _scope = _fileScope('topicPublishHistory');
+    this.logger.debug(_scope, 'called', { topicId, days })
+
+    const events = this.statement.topicPublishHistory.all({ topicId, daysAgo: days });
+    const history = Array.from({ length: days }, () => 0);
+    events.forEach(({ daysAgo, contentUpdates }) => history[daysAgo] = Number(contentUpdates));
+
+    return history;
+  }
+
+
   topicSet(dbCtx, data) {
     const _scope = _fileScope('topicSet');
     this.logger.debug(_scope, 'called', data);
diff --git a/src/db/sqlite/sql/schema/1.0.0/er.dot b/src/db/sqlite/sql/schema/1.0.0/er.dot
new file mode 100644 (file)
index 0000000..9793ad3
--- /dev/null
@@ -0,0 +1,125 @@
+digraph WebsubHubERD {
+       graph[
+               rankdir=LR,
+               overlap=false,
+               splines=true,
+               label="Websub Hub Entity-Relations\nSQLite\nSchema 1.0.0",
+               labelloc="t",
+               fontsize=26,
+       ];
+       // layout=neato;
+       node[shape=plain];
+       edge[arrowhead=crow];
+
+       topic [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">url</td></tr>
+               <tr><td port="">lease_seconds_preferred</td></tr>
+               <tr><td port="">lease_seconds_min</td></tr>
+               <tr><td port="">lease_seconds_max</td></tr>
+               <tr><td port="">publisher_validation_url</td></tr>
+               <tr><td port="">content_hash_algorithm</td></tr>
+               <tr><td port="">is_active</td></tr>
+               <tr><td port="">is_deleted</td></tr>
+               <tr><td port="">last_publish</td></tr>
+               <tr><td port="">content_fetch_next_attempt</td></tr>
+               <tr><td port="">content_fetch_attempts_since_success</td></tr>
+               <tr><td port="">content_updated</td></tr>
+               <tr><td port="">content</td></tr>
+               <tr><td port="">content_hash</td></tr>
+               <tr><td port="">content_type</td></tr>
+       </table>
+       >];
+
+       topic_fetch_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+       subscription [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">verified</td></tr>
+               <tr><td port="">expires</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">content_delivered</td></tr>
+               <tr><td port="">delivery_attempts_since_success</td></tr>
+               <tr><td port="">delivery_next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> subscription:fk_topic_id [ arrowhead = crow ];
+
+       subscription_delivery_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+       verification [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">mode</td></tr>
+               <tr><td port="">reason</td></tr>
+               <tr><td port="">lease_seconds</td></tr>
+               <tr><td port="">is_publisher_validated</td></tr>
+               <tr><td port="">request_id</td></tr>
+               <tr><td port="">attempts</td></tr>
+               <tr><td port="">next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> verification:fk_topic_id;
+
+       verification_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       verification:pk_id -> verification_in_progress:fk_id;
+       topic:pk_id -> verification_in_progress:fk_topic_id;
+
+       authentication [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">last_authenticated</td></tr>
+               <tr><td port="">identifier</td></tr>
+               <tr><td port="">credential</td></tr>
+       </table>
+       >];
+
+}
\ No newline at end of file
diff --git a/src/db/sqlite/sql/schema/1.0.1/er.dot b/src/db/sqlite/sql/schema/1.0.1/er.dot
new file mode 100644 (file)
index 0000000..103e54a
--- /dev/null
@@ -0,0 +1,125 @@
+digraph WebsubHubERD {
+       graph[
+               rankdir=LR,
+               overlap=false,
+               splines=true,
+               label="Websub Hub Entity-Relations\SQLite\nSchema 1.0.1",
+               labelloc="t",
+               fontsize=26,
+       ];
+       // layout=neato;
+       node[shape=plain];
+       edge[arrowhead=crow];
+
+       topic [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">url</td></tr>
+               <tr><td port="">lease_seconds_preferred</td></tr>
+               <tr><td port="">lease_seconds_min</td></tr>
+               <tr><td port="">lease_seconds_max</td></tr>
+               <tr><td port="">publisher_validation_url</td></tr>
+               <tr><td port="">content_hash_algorithm</td></tr>
+               <tr><td port="">is_active</td></tr>
+               <tr><td port="">is_deleted</td></tr>
+               <tr><td port="">last_publish</td></tr>
+               <tr><td port="">content_fetch_next_attempt</td></tr>
+               <tr><td port="">content_fetch_attempts_since_success</td></tr>
+               <tr><td port="">content_updated</td></tr>
+               <tr><td port="">content</td></tr>
+               <tr><td port="">content_hash</td></tr>
+               <tr><td port="">content_type</td></tr>
+       </table>
+       >];
+
+       topic_fetch_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+       subscription [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">verified</td></tr>
+               <tr><td port="">expires</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">content_delivered</td></tr>
+               <tr><td port="">delivery_attempts_since_success</td></tr>
+               <tr><td port="">delivery_next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> subscription:fk_topic_id;
+
+       subscription_delivery_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+       verification [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">mode</td></tr>
+               <tr><td port="">reason</td></tr>
+               <tr><td port="">lease_seconds</td></tr>
+               <tr><td port="">is_publisher_validated</td></tr>
+               <tr><td port="">request_id</td></tr>
+               <tr><td port="">attempts</td></tr>
+               <tr><td port="">next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> verification:fk_topic_id;
+
+       verification_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       verification:pk_id -> verification_in_progress:fk_id;
+       topic:pk_id -> verification_in_progress:fk_topic_id;
+
+       authentication [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">last_authenticated</td></tr>
+               <tr><td port="">identifier</td></tr>
+               <tr><td port="">credential</td></tr>
+       </table>
+       >];
+
+}
\ No newline at end of file
diff --git a/src/db/sqlite/sql/schema/1.0.2/er.dot b/src/db/sqlite/sql/schema/1.0.2/er.dot
new file mode 100644 (file)
index 0000000..82cb5b1
--- /dev/null
@@ -0,0 +1,126 @@
+digraph WebsubHubERD {
+       graph[
+               rankdir=LR,
+               overlap=false,
+               splines=true,
+               label="Websub Hub Entity-Relations\nSQLite\nSchema 1.0.2",
+               labelloc="t",
+               fontsize=26,
+       ];
+       // layout=neato;
+       node[shape=plain];
+       edge[arrowhead=crow];
+
+       topic [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">url</td></tr>
+               <tr><td port="">lease_seconds_preferred</td></tr>
+               <tr><td port="">lease_seconds_min</td></tr>
+               <tr><td port="">lease_seconds_max</td></tr>
+               <tr><td port="">publisher_validation_url</td></tr>
+               <tr><td port="">content_hash_algorithm</td></tr>
+               <tr><td port="">is_active</td></tr>
+               <tr><td port="">is_deleted</td></tr>
+               <tr><td port="">last_publish</td></tr>
+               <tr><td port="">content_fetch_next_attempt</td></tr>
+               <tr><td port="">content_fetch_attempts_since_success</td></tr>
+               <tr><td port="">content_updated</td></tr>
+               <tr><td port="">content</td></tr>
+               <tr><td port="">content_hash</td></tr>
+               <tr><td port="">content_type</td></tr>
+       </table>
+       >];
+
+       topic_fetch_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+       subscription [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">verified</td></tr>
+               <tr><td port="">expires</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">content_delivered</td></tr>
+               <tr><td port="">latest_content_delivered</td></tr>
+               <tr><td port="">delivery_attempts_since_success</td></tr>
+               <tr><td port="">delivery_next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> subscription:fk_topic_id;
+
+       subscription_delivery_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+       verification [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">mode</td></tr>
+               <tr><td port="">reason</td></tr>
+               <tr><td port="">lease_seconds</td></tr>
+               <tr><td port="">is_publisher_validated</td></tr>
+               <tr><td port="">request_id</td></tr>
+               <tr><td port="">attempts</td></tr>
+               <tr><td port="">next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> verification:fk_topic_id;
+
+       verification_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       verification:pk_id -> verification_in_progress:fk_id;
+       topic:pk_id -> verification_in_progress:fk_topic_id;
+
+       authentication [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">last_authenticated</td></tr>
+               <tr><td port="">identifier</td></tr>
+               <tr><td port="">credential</td></tr>
+       </table>
+       >];
+
+}
\ No newline at end of file
diff --git a/src/db/sqlite/sql/schema/1.0.3/er.dot b/src/db/sqlite/sql/schema/1.0.3/er.dot
new file mode 100644 (file)
index 0000000..0fe277b
--- /dev/null
@@ -0,0 +1,137 @@
+digraph WebsubHubERD {
+       graph[
+               rankdir=LR,
+               overlap=false,
+               splines=true,
+               label="Websub Hub Entity-Relations\nSQLite\nSchema 1.0.3",
+               labelloc="t",
+               fontsize=26,
+       ];
+       // layout=neato;
+       node[shape=plain];
+       edge[arrowhead=crow];
+
+       topic [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">url</td></tr>
+               <tr><td port="">lease_seconds_preferred</td></tr>
+               <tr><td port="">lease_seconds_min</td></tr>
+               <tr><td port="">lease_seconds_max</td></tr>
+               <tr><td port="">publisher_validation_url</td></tr>
+               <tr><td port="">content_hash_algorithm</td></tr>
+               <tr><td port="">is_active</td></tr>
+               <tr><td port="">is_deleted</td></tr>
+               <tr><td port="">last_publish</td></tr>
+               <tr><td port="">content_fetch_next_attempt</td></tr>
+               <tr><td port="">content_fetch_attempts_since_success</td></tr>
+               <tr><td port="">content_updated</td></tr>
+               <tr><td port="">content</td></tr>
+               <tr><td port="">content_hash</td></tr>
+               <tr><td port="">content_type</td></tr>
+       </table>
+       >];
+
+       topic_fetch_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+       topic_content_history [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+                       <tr><td border="2" bgcolor="lightblue">TOPIC_CONTENT_HISTORY</td></tr>
+                       <tr><td port="fk_topic_id">topic_id</td></tr>
+                       <tr><td port="">content_updated</td></tr>
+                       <tr><td port="">content_size</td></tr>
+                       <tr><td port="">content_hash</td></tr>
+       </table>
+       >];
+       topic:pk_id -> topic_content_history:fk_topic_id;
+
+       subscription [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">verified</td></tr>
+               <tr><td port="">expires</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">content_delivered</td></tr>
+               <tr><td port="">latest_content_delivered</td></tr>
+               <tr><td port="">delivery_attempts_since_success</td></tr>
+               <tr><td port="">delivery_next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> subscription:fk_topic_id;
+
+       subscription_delivery_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+       verification [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+               <tr><td port="pk_id">id</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">secret</td></tr>
+               <tr><td port="">signature_algorithm</td></tr>
+               <tr><td port="">http_remote_addr</td></tr>
+               <tr><td port="">http_from</td></tr>
+               <tr><td port="">mode</td></tr>
+               <tr><td port="">reason</td></tr>
+               <tr><td port="">lease_seconds</td></tr>
+               <tr><td port="">is_publisher_validated</td></tr>
+               <tr><td port="">request_id</td></tr>
+               <tr><td port="">attempts</td></tr>
+               <tr><td port="">next_attempt</td></tr>
+       </table>
+       >];
+       topic:pk_id -> verification:fk_topic_id;
+
+       verification_in_progress [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+               <tr><td port="fk_id">id</td></tr>
+               <tr><td port="fk_topic_id">topic_id</td></tr>
+               <tr><td port="">callback</td></tr>
+               <tr><td port="">claimant</td></tr>
+               <tr><td port="">claimed</td></tr>
+               <tr><td port="">claim_expires</td></tr>
+       </table>
+       >];
+       verification:pk_id -> verification_in_progress:fk_id;
+       topic:pk_id -> verification_in_progress:fk_topic_id;
+
+       authentication [label=<
+       <table cellspacing="0" cellborder="1" border="0">
+               <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+               <tr><td port="">created</td></tr>
+               <tr><td port="">last_authenticated</td></tr>
+               <tr><td port="">identifier</td></tr>
+               <tr><td port="">credential</td></tr>
+       </table>
+       >];
+
+}
\ No newline at end of file
diff --git a/src/db/sqlite/sql/topic-publish-history.sql b/src/db/sqlite/sql/topic-publish-history.sql
new file mode 100644 (file)
index 0000000..184c449
--- /dev/null
@@ -0,0 +1,12 @@
+--
+SELECT
+       (strftime('%s', 'now') - content_updated) / 86400 AS days_ago,
+       count(*) AS content_updates
+FROM topic_content_history
+WHERE
+       days_ago <= :daysAgo
+AND
+       (:topicId IS NULL OR topic_id = :topicId)
+GROUP BY days_ago
+ORDER BY days_ago
+;
index b5eff7a3ecb88698beeb9d4bcc9364f4ccedacb7..c570671d94ada2bad567fc02f5598d923d90b04b 100644 (file)
@@ -544,10 +544,65 @@ class Manager {
     });
 
     res.end(this.infoContent(ctx));
-    this.logger.info(_scope, 'finished', { ...ctx });
+    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('getHist');
+    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 });
+  }
+
   /**
    * GET request for authorized /admin information.
    * @param {http.ServerResponse} res
@@ -572,7 +627,7 @@ class Manager {
     }
 
     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 });
   }
 
 
@@ -585,12 +640,17 @@ class Manager {
     const _scope = _fileScope('getTopicDetails');
     this.logger.debug(_scope, 'called', { ctx });
 
+
+    ctx.publishSpan = 60;
     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);
+    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) {
@@ -603,7 +663,7 @@ class Manager {
     }
 
     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 });
   }
 
 
index 17fa134d738447deb29aa5e889aaf806cb1443f8..cfed60c5660ddd1b1a9f2691cd64ee77c7048d17 100644 (file)
@@ -52,6 +52,7 @@ class Service extends Dingus {
     this.on(['GET', 'HEAD'], '/admin', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/admin/`);
     this.on(['GET', 'HEAD'], '/admin/', this.handlerGetAdminOverview.bind(this));
     this.on(['GET', 'HEAD'], '/admin/topic/:topicId', this.handlerGetAdminTopicDetails.bind(this));
+    this.on(['GET', 'HEAD'], '/admin/topic/:topicId/history.svg', this.handlerGetHistorySVG.bind(this));
 
     // Private data-editing endpoints
     this.on(['PATCH', 'DELETE'], '/admin/topic/:topicId', this.handlerUpdateTopic.bind(this));
@@ -68,24 +69,6 @@ class Service extends Dingus {
 
   }
 
-  /**
-   * Wrap the Dingus head handler, to remove the response body from the context,
-   * lest it be logged.
-   * @param {http.ClientRequest} req
-   * @param {http.ServerResponse} res
-   * @param {object} ctx
-   */
-  static setHeadHandler(req, res, ctx) {
-    if (req.method === 'HEAD') {
-      Dingus.setHeadHandler(req, res, ctx);
-      const origEnd = res.end.bind(res);
-      res.end = function (data, encoding, ...rest) {
-        const origResult = origEnd(data, encoding, ...rest);
-        delete ctx.responseBody;
-        return origResult;
-      };
-    }
-  }
 
   /**
    * @param {http.ClientRequest} req
@@ -115,7 +98,7 @@ class Service extends Dingus {
     ];
     this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
 
-    Service.setHeadHandler(req, res, ctx);
+    Dingus.setHeadHandler(req, res, ctx);
 
     this.setResponseType(responseTypes, req, res, ctx);
 
@@ -134,7 +117,7 @@ class Service extends Dingus {
     const _scope = _fileScope('handlerGetHealthcheck');
     this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
   
-    Service.setHeadHandler(req, res, ctx);
+    Dingus.setHeadHandler(req, res, ctx);
 
     this.setResponseType(this.responseTypes, req, res, ctx);
 
@@ -153,7 +136,7 @@ class Service extends Dingus {
 
     const responseTypes = [...this.responseTypes, Enum.ContentType.ImageSVG];
 
-    Service.setHeadHandler(req, res, ctx);
+    Dingus.setHeadHandler(req, res, ctx);
 
     this.setResponseType(responseTypes, req, res, ctx);
 
@@ -161,6 +144,20 @@ class Service extends Dingus {
   }
 
 
+  async handlerGetHistorySVG(req, res, ctx) {
+    const _scope = _fileScope('handlerGetHist');
+    this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
+
+    const responseTypes = [Enum.ContentType.ImageSVG];
+
+    Dingus.setHeadHandler(req, res, ctx);
+
+    this.setResponseType(responseTypes, req, res, ctx);
+
+    await this.manager.getHistorySVG(res, ctx);
+  }
+
+
   /**
    * @param {http.ClientRequest} req
    * @param {http.ServerResponse} res
@@ -170,7 +167,7 @@ class Service extends Dingus {
     const _scope = _fileScope('handlerGetAdminOverview');
     this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
 
-    Service.setHeadHandler(req, res, ctx);
+    Dingus.setHeadHandler(req, res, ctx);
 
     this.setResponseType(this.responseTypes, req, res, ctx);
 
@@ -189,7 +186,7 @@ class Service extends Dingus {
     const _scope = _fileScope('handlerGetAdminTopicDetails');
     this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
 
-    Service.setHeadHandler(req, res, ctx);
+    Dingus.setHeadHandler(req, res, ctx);
 
     this.setResponseType(this.responseTypes, req, res, ctx);
 
@@ -200,20 +197,15 @@ class Service extends Dingus {
 
 
   /**
-   * Similar to super.ingestBody, but if no body was sent, do not parse (and
-   * thus avoid possible unsupported media type error).
-   * Also removes raw body from context, to simplify scrubbing sensitive data from logs.
+   * If no body was sent, do not parse (and thus avoid possible unsupported media type error).
    * @param {http.ClientRequest} req
    * @param {http.ServerResponse} res
    * @param {Object} ctx
    */
   async maybeIngestBody(req, res, ctx) {
-    ctx.rawBody = await this.bodyData(req);
-    const contentType = Dingus.getRequestContentType(req);
-    if (ctx.rawBody) {
-      this.parseBody(contentType, ctx);
-      delete ctx.rawBody;
-    }
+    return super.ingestBody(req, res, ctx, {
+      parseEmptyBody: false,
+    });
   }
 
 
@@ -282,7 +274,7 @@ class Service extends Dingus {
     const _scope = _fileScope('handlerGetAdminLogin');
     this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
 
-    Service.setHeadHandler(req, res, ctx);
+    Dingus.setHeadHandler(req, res, ctx);
 
     this.setResponseType(this.responseTypes, req, res, ctx);
 
index 525337dc5ad6e76b3591d6d6297717009cc0d1e4..1ec3d110275d2197f20f9a239a6a4776922b84b6 100644 (file)
@@ -26,7 +26,6 @@ module.exports = (ctx, options) => {
   };
 
   const content = [
-    '<script>0</script>', // This fixes a layout rendering flash on load in FF; do not know why this works but it does.
     `      <section class="topics">
         <p>${ctx.topics.length ? ctx.topics.length : 'no'} topic${(ctx.topics.length === 1) ? '' : 's'}</p>
         <table>
index 2b9a1af7fb44400768ffa9e416084651a034c525..abf441adfe8178d5d2a1104e0b848791fc023c16 100644 (file)
@@ -34,7 +34,6 @@ module.exports = (ctx, options) => {
   };
 
   const content = [
-    '<script>0</script>', // This fixes a layout rendering flash on load in FF; do not know why this works but it does.
     `      <section class="topics">
         <table>
           <thead>`,
@@ -45,6 +44,10 @@ module.exports = (ctx, options) => {
     `        </tbody>
         </table>
       </section>`,
+    `      <section class="history">
+        <p>Topic Publish History &mdash; ${ctx.publishCount} updates in the last ${ctx.publishSpan} days</p>
+        <img title="Topic Publish History" src="${ctx.params.topicId}/history.svg">
+      </section>`,
     `      <section class="subscriptions">
         <p>${ctx.subscriptions.length ? ctx.subscriptions.length : 'no'} subscription${(ctx.subscriptions.length === 1) ? '' : 's'}</p>
         <table>
index 681e0720a2c3a001745655a4426a0a3123387c83..3ef44d26533b1eb8247104c55bc6ab34e7396f36 100644 (file)
@@ -1,19 +1,6 @@
 'use strict';
 
-function escapeXml(s) {
-  if (typeof s === 'number') {
-    return s;
-  } else if (typeof s !== 'string') {
-    return undefined;
-  } else {
-    return s
-      .replace(/&/g, '&amp;')
-      .replace(/</g, '&lt;')
-      .replace(/>/g, '&gt;')
-      .replace(/"/g, '&quot;')
-      .replace(/'/g, '&apos;');
-  }
-}
+const th = require('./template-helper');
 
 
 const ctxDefaults = {
@@ -57,8 +44,8 @@ module.exports = (ctx, label, message, accessibleText) => {
    * renderer from https://github.com/badges/shields/tree/master/badge-maker which
    * is under the http://creativecommons.org/publicdomain/zero/1.0/ license.
    */
-  return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${ctx.width}" height="${ctx.height}" role="img" aria-label="${escapeXml(ctx.accessibleText)}">
-  <title>${escapeXml(ctx.accessibleText)}</title>
+  return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${ctx.width}" height="${ctx.height}" role="img" aria-label="${th.xmlEscape(ctx.accessibleText)}">
+  <title>${th.xmlEscape(ctx.accessibleText)}</title>
   <linearGradient id="s" x2="0" y2="100%">
     <stop offset="0"  stop-color="#fff" stop-opacity=".7"/>
     <stop offset=".1" stop-color="#aaa" stop-opacity=".1"/>
@@ -74,8 +61,8 @@ module.exports = (ctx, label, message, accessibleText) => {
     <rect width="${ctx.width}" height="${ctx.height}" fill="url(#s)"/>
   </g>
   <g fill="${ctx.color}" text-anchor="left" font-family="${ctx.fontFamily}" text-rendering="geometricPrecision" font-size="11" font-weight="bold">
-    <text x="${ctx.halfCharWidth}" y="${ctx.verticalMargin}">${escapeXml(ctx.label)}</text>
-    <text x="${fixedRound(ctx.halfCharWidth + ctx.labelWidth)}" y="${ctx.verticalMargin}">${escapeXml(ctx.message)}</text>
+    <text x="${ctx.halfCharWidth}" y="${ctx.verticalMargin}">${th.xmlEscape(ctx.label)}</text>
+    <text x="${fixedRound(ctx.halfCharWidth + ctx.labelWidth)}" y="${ctx.verticalMargin}">${th.xmlEscape(ctx.message)}</text>
   </g>
 </svg>`;
 };
diff --git a/src/template/histogram-svg.js b/src/template/histogram-svg.js
new file mode 100644 (file)
index 0000000..13d0c91
--- /dev/null
@@ -0,0 +1,131 @@
+'use strict';
+
+const th = require('./template-helper');
+
+const optionsDefaults = {
+  barWidth: 20,
+  barHeight: 100,
+  scaleBars: true,
+  barCaptionFn: () => '',
+  labelZero: undefined,
+  labelX: undefined,
+  labelHeight: 8,
+  fontFamily: 'DejaVu Sans,Verdana,Geneva,sans-serif',
+  frameColor: 'gray',
+  tickEvery: undefined,
+  tickHeight: 4,
+  tickColor: 'gray',
+  minItems: 0,
+  maxItems: undefined,
+};
+
+function grey(percent) {
+  const value = Math.round(95 * (1.0 - percent));
+  return `rgb(${value}%, ${value}%, ${value}%)`;
+}
+
+function svgHeader(options, width, height) {
+  return [
+    `<svg
+version="1.1"
+baseProfile="full"
+xmlns="http://www.w3.org/2000/svg"
+xmlns:xlink="http://www.w3.org/1999/xlink"
+xmlns:ev="http://www.w3.org/2001/xml-events"
+width="${width}px" height="${height}px"
+viewBox="0 0 ${width} ${height}"
+preserveAspectRatio="xMinYMin meet"`,
+    options.accessibleText ? ` role="img" aria-label="${th.xmlEscape(options.accessibleText)}"` : '',
+    '>',
+    options.title ? `<title>${th.xmlEscape(options.title)}</title>` : '',
+    options.description ? `<desc>${th.xmlEscape(options.description)}</desc>` : '',
+    '\n',
+  ];
+}
+
+function svgFrame(options, width) {
+  if (!options.frameColor) {
+    return '';
+  }
+  return `\t<g id="frame">
+\t\t<rect x="0.5px" y="0.5px" width="${width - 1}px" height="${options.barHeight - 1}px" fill="none" stroke="${options.frameColor}" />
+\t</g>\n`;
+}
+
+function svgTicks(options, width) {
+  if (!options.tickEvery) {
+    return '';
+  }
+  const tickSpacing = options.tickEvery * options.barWidth;
+  const tickOffset = options.barWidth / 2;
+  const numTicks = Math.ceil(width / tickSpacing);
+  const ticks = Array.from({ length: numTicks }, (_, i) => i)
+    .map((index) => `M ${(index * tickSpacing) + tickOffset} ${options.barHeight - 2} v ${options.tickHeight}`)
+    .join(' ');
+
+  return `\t<g id="ticks">
+\t<path d="${ticks}" stroke="${options.tickColor}" fill="none" stroke-width="0.5px" />
+\t</g>\n`;
+}
+
+function svgLabels(options, width, height) {
+  const labels = [];
+  if (!options.labelHeight) {
+    return labels;
+  }
+  labels.push(`\t<g font-size="${options.labelHeight}px" font-family="${options.fontFamily}" font-variant="small-caps">\n`);
+  const y = height - (options.labelHeight / 2) + 2;
+  if (options.labelZero) {
+    labels.push(`\t\t<text font-size="${options.labelHeight}px" text-anchor="start" x="0" y="${y}">${options.labelZero}</text>\n`);
+  }
+  if (options.labelX) {
+    labels.push(`\t\t<text text-anchor="middle" x="${width / 2}" y="${y}">${options.labelX}</text>\n`);
+  }
+  labels.push('\t</g>\n');
+  return labels;
+}
+
+function svgFooter() {
+  return '</svg>';
+}
+
+function svgBar(options, value, index, maxValue) {
+  const id = `i${index}`;
+  const x = options.barWidth * index;
+  const width = options.barWidth;
+  const height = options.barHeight;
+  const scale = value / Math.max(1, maxValue);
+  const scaleHeight = options.scaleBars ? height * scale : height;
+  const yOffset = height - scaleHeight;
+  const fill = grey(scale);
+  const emptyFill =  grey(0);
+  const title = th.xmlEscape(options.barCaptionFn(index, value));
+  return [
+    ...(options.scaleBars && [
+      `\t<rect id="${id}" x="${x}" y="0" width="${width}" height="${height}" fill="${emptyFill}">`,
+      ...(title && `<title>${title}</title>`),
+      '\t</rect>\n',
+    ]),
+    `\t<rect id="${id}" x="${x}" y="${yOffset}" width="${width}" height="${scaleHeight}" fill="${fill}">`,
+    ...(title && `<title>${title}</title>`),
+    '\t</rect>\n',
+  ].join('');
+}
+
+module.exports = (items, options = {}) => {
+  options = Object.assign({}, optionsDefaults, options);
+
+  const maxValue = items.reduce((a, b) => Math.max(a, b), 0);
+  const hasLabel = !!options.labelX || !!options.labelZero;
+  const height = options.barHeight + 2 + (hasLabel ? options.labelHeight + 2 : 0);
+  const width = Math.max(items.length, options.minItems) * options.barWidth;
+
+  return [
+    ...svgHeader(options, width, height),
+    ...items.slice(0, options.maxItems).map((value, index) => svgBar(options, value, index, maxValue)),
+    svgFrame(options, width),
+    svgTicks(options, width),
+    ...svgLabels(options, width, height),
+    svgFooter(),
+  ].join('');
+};
\ No newline at end of file
index e177788079d180ccf598fbdef9a04b400fce5784..9ae40d25709502d9a333f200b2d73e8fe6f08a24 100644 (file)
@@ -4,5 +4,6 @@ module.exports = {
   adminOverviewHTML: require('./admin-overview-html'),
   adminTopicDetailsHTML: require('./admin-topic-details-html'),
   badgeSVG: require('./badge-svg'),
+  histogramSVG: require('./histogram-svg'),
   rootHTML: require('./root-html'),
 };
\ No newline at end of file
index 6771fdd91b8103e6b667d4eef52daccbf8559b31..9496c7e8a3a5a666d5be34591380760bb35f81a4 100644 (file)
@@ -160,7 +160,6 @@ module.exports = (ctx, options) => {
     navLinks,
   };
   const content = [
-    '<script>0</script>', // This fixes a layout rendering flash on load in FF; do not know why this works but it does.
     aboutSection(),
     usageSection(isPublicHub, hubURL),
     contactSection(contactHTML),
index bb4dad1e3f574f2332683c9e4bc990c9bc98b3a2..a3e113179928d3a878400c9b9c513961a930f2c9 100644 (file)
@@ -2,7 +2,6 @@
 
 const { TemplateHelper } = require('@squeep/html-template-helper');
 
-
 /**
  * Render a topic as a row of details.
  * @param {Object} topic
@@ -112,7 +111,29 @@ function renderSubscriptionRowHeader() {
 }
 
 
+/**
+ * Escape some xml things in strings.
+ * @param {String} string
+ */
+function xmlEscape(string) {
+  if (typeof string === 'number') {
+    return string;
+  }
+  if (typeof string !== 'string') {
+    return undefined;
+  }
+  // eslint-disable-next-line security/detect-object-injection
+  return string.replace(/[<>&'"]/, (c) => ({
+    '<': '&lt;',
+    '>': '&gt;',
+    '&': '&amp;',
+    '\'': '&apos;',
+    '"': '&quot;',
+  }[c]));
+}
+
 module.exports = Object.assign(Object.create(TemplateHelper), {
+  xmlEscape,
   renderTopicRowHeader,
   renderTopicRow,
   renderSubscriptionRowHeader,
index fb85b52b1834bd91ae5ed426d585fd0eb65780a9..f6099e2a74e8bcacac6858ec77daf3bf82e413b5 100644 (file)
@@ -69,6 +69,9 @@ section + section {
 }
 .topics {}
 .subscriptions {}
+.history img {
+       width: 100%;
+}
 .external {
        background-image: url("external-link.svg");
        background-position: right center;
index 786365722b13880f698b4f46184b3d4f3749bda2..74e58349159aecb7ce05f8210fe83d51e847b2bd 100644 (file)
@@ -173,13 +173,13 @@ describe('DatabaseBase', function () {
       db._topicSetDataValidate(data);
     });
     it('covers invalid value', function () {
-     data.leaseSecondsPreferred = -100;
-     try {
-       db._topicSetDataValidate(data);
-       assert.fail('did not get expected exception');
-     } catch (e) {
-       assert(e instanceof DBErrors.DataValidation);
-     }
+      data.leaseSecondsPreferred = -100;
+      try {
+        db._topicSetDataValidate(data);
+        assert.fail('did not get expected exception');
+      } catch (e) {
+        assert(e instanceof DBErrors.DataValidation);
+      }
     });
     it('covers invalid range', function () {
       data.leaseSecondsPreferred = 10000;
index 83feac761222a620793cb6963b9a2604e3fb60f5..b92202d41b0e30a04e759872ee984e756f95a13c 100644 (file)
@@ -79,6 +79,11 @@ describe('Database Integration', function () {
         assert(db);
       });
 
+      it('is healthy', async function () {
+        const result = await db.healthCheck();
+        assert(result);
+      });
+
       describe('Authentication', function () {
         let identifier, credential;
         beforeEach(function () {
@@ -220,6 +225,14 @@ describe('Database Integration', function () {
             assert.strictEqual(Number(topic.contentFetchAttemptsSinceSuccess), 0);
           });
         });
+        step('gets publish history', async function () {
+          await db.context(async (dbCtx) => {
+            const result = (await db.topicPublishHistory(dbCtx, topicId, 7))
+              .map((x) => Number(x));
+            const expected = [1, 0, 0, 0, 0, 0, 0];
+            assert.deepStrictEqual(result, expected);
+          });  
+        });
         step('deletes a topic', async function () {
           await db.context(async (dbCtx) => {
             const result = await db.topicSet(dbCtx, testData.anotherTopicSet);
index da071f2ab28566b53b73ba3ff25858c7e96e0355..626b0c2573a145f1727e6cc5e099e06e16d1cdc1 100644 (file)
@@ -1322,6 +1322,21 @@ describe('DatabasePostgres', function () {
     });
   });
 
+  describe('topicPublishHistory', function () {
+    beforeEach(function () {
+      sinon.stub(db.db, 'manyOrNone');
+    });
+    it('success', async function () {
+      db.db.manyOrNone.returns([
+        { daysAgo: 1, contentUpdates: 1 },
+        { daysAgo: 3, contentUpdates: 2 },
+      ]);
+      const result = await db.topicPublishHistory(dbCtx, topicId, 7);
+      const expected = [0, 1, 0, 2, 0, 0, 0];
+      assert.deepStrictEqual(result, expected);
+    });
+  }); // topicPublishHistory
+
   describe('topicSet', function () {
     let data;
     beforeEach(function () {
index be6399445bcb01279ca2c2b9392c58cbf94e934c..deb0ee244fbbb0811f0f6b62b66bebe41aac8ff6 100644 (file)
@@ -460,7 +460,7 @@ describe('DatabaseSQLite', function () {
         assert.deepStrictEqual(e, expected);
       }
     });
-  });
+  }); // subscriptionDeleteExpired
 
   describe('subscriptionDeliveryClaim', function () {
     it('success', async function () {
@@ -1151,7 +1151,22 @@ describe('DatabaseSQLite', function () {
       }
       assert(db.statement.topicDeleteById.run.called);
     });
-  });
+  }); // topicPendingDelete
+
+  describe('topicPublishHistory', function () {
+    beforeEach(function () {
+      sinon.stub(db.statement.topicPublishHistory, 'all');
+    });
+    it('success', function () {
+      db.statement.topicPublishHistory.all.returns([
+        { daysAgo: 1, contentUpdates: 1 },
+        { daysAgo: 3, contentUpdates: 2 },
+      ]);
+      const result = db.topicPublishHistory(dbCtx, topicId, 7);
+      const expected = [0, 1, 0, 2, 0, 0, 0];
+      assert.deepStrictEqual(result, expected);
+    });
+  }); // topicPublishHistory
 
   describe('topicSet', function () {
     let data;
index 67283d76b82c992058e39b2926bd699cd1d43830..cbd83243d3faecabdf8d77667151a230ac180f88 100644 (file)
@@ -33,6 +33,7 @@ describe('Manager', function () {
     };
     ctx = {
       params: {},
+      queryParams: {},
     };
     manager = new Manager(stubLogger, stubDb, options);
     sinon.stub(manager.communication, 'verificationProcess');
@@ -137,6 +138,31 @@ describe('Manager', function () {
     });
   }); // getInfo
 
+  describe('_historyBarCaption', function () {
+    it('covers today, none', function () {
+      const result = Manager._historyBarCaption(0, 0);
+      assert.strictEqual(result, 'today, no updates');
+    });
+    it('covers yesterday, singular', function () {
+      const result = Manager._historyBarCaption(1, 1);
+      assert.strictEqual(result, 'yesterday, 1 update');
+    });
+    it('covers older, plural', function () {
+      const result = Manager._historyBarCaption(7, 3);
+      assert.strictEqual(result, '7 days ago, 3 updates');
+    });
+  }); // _historyBarCaption
+
+  describe('getHistorySVG', function () {
+    beforeEach(function () {
+      manager.db.topicPublishHistory.resolves([0, 1, 2, 1, 0, 1, 2, 0, 1]);
+    });
+    it('covers', async function () {
+      await manager.getHistorySVG(res, ctx);
+      assert(res.end.called);
+    });
+  }); // getHistorySVG
+
   describe('getAdminOverview', function () {
     beforeEach(function () {
       manager.db.topicGetAll.resolves([
@@ -212,6 +238,7 @@ describe('Manager', function () {
         deliveryAttemptsSinceSuccess: 0,
         deliveryNextAttempt: new Date(-Infinity),
       }]);
+      manager.db.topicPublishHistory.resolves([0, 1, 0, 1, 0]);
     });
     it('covers', async function() {
       await manager.getTopicDetails(res, ctx);
index cafe1d16922ff9e4e0f48b19921a572e39d7ea2f..9afaf13d0242e839a433a969515d08c5884d726f 100644 (file)
@@ -61,20 +61,6 @@ describe('Service', function () {
     });
   }); // maybeIngestBody
 
-  describe('setHeadHandler', function () {
-    it('covers', function () {
-      const origEnd = res.end;
-      sinon.stub(Service.__proto__, 'setHeadHandler');
-      ctx.responseBody = 'data';
-      req.method = 'HEAD';
-      Service.setHeadHandler(req, res, ctx);
-      res.end('foop');
-      assert(Service.__proto__.setHeadHandler.called);
-      assert(origEnd.called);
-      assert(!('responseBody' in ctx));
-    });
-  }); // setHeadHandler
-
   describe('handlerPostRoot', function () {
     it('covers public mode', async function () {
       await service.handlerPostRoot(req, res, ctx);
@@ -108,12 +94,19 @@ describe('Service', function () {
   }); // handlerGetHealthcheck
 
   describe('handlerGetInfo', function () {
-    it('covers', async function() {
+    it('covers', async function () {
       await service.handlerGetInfo(req, res, ctx);
       assert(service.manager.getInfo.called);
     });
   }); // handlerGetInfo
 
+  describe('handlerGetHistorySVG', function () {
+    it('covers', async function () {
+      await service.handlerGetHistorySVG(req, res, ctx);
+      assert(service.manager.getHistorySVG.called);
+    });
+  }); // handlerGetHistorySVG
+
   describe('handlerGetAdminOverview', function () {
     it('covers', async function () {
       await service.handlerGetAdminOverview(req, res, ctx);
index 823b439d695ecbe41566b40d91a38382fae433b4..7795c2b257e849cd9294903d08255f5880f832d7 100644 (file)
@@ -11,6 +11,9 @@ describe('Admin Topic Details HTML Template', function () {
 
   beforeEach(function () {
     ctx = {
+      params: {
+        topicId: '97dd5488-a303-11ec-97ab-0025905f714a',
+      },
       topic: {},
       subscriptions: [
         {},
diff --git a/test/src/template/histogram-svg.js b/test/src/template/histogram-svg.js
new file mode 100644 (file)
index 0000000..656ccdb
--- /dev/null
@@ -0,0 +1,29 @@
+/* eslint-env mocha */
+'use strict';
+
+const assert = require('assert');
+const template = require('../../../src/template/histogram-svg');
+
+describe('Histogram SVG Template', function () {
+  let items, options;
+  beforeEach(function () {
+    items = [];
+    options = {};
+  });
+  it('covers defaults', function () {
+    const result = template(items, options);
+    assert(result);
+  });
+  it('covers options', function () {
+    items = [0, 1, 2];
+    options = {
+      labelX: 'Days Ago',
+      labelZero: 'Today',
+      tickEvery: 2,
+      frameColor: undefined,
+      labelHeight: 0,
+    };
+    const result = template(items, options);
+    assert(result);
+  });
+});
\ No newline at end of file
index 8c8579d1cc5a83b3d65b8acd2c232805d61097a8..5a62a7c4b15f4d68a9c12f1a61461eda6c8c520d 100644 (file)
@@ -63,4 +63,19 @@ describe('Template Helper', function () {
     });
   }); // renderSubscriptionRowHeader
 
+  describe('xmlEscape', function () {
+    it('ignores numbers', function () {
+      const result = th.xmlEscape(3);
+      assert.strictEqual(result, 3);
+    });
+    it('ignores objects', function () {
+      const result = th.xmlEscape({});
+      assert.strictEqual(result, undefined);
+    });
+    it('escapes a thing', function () {
+      const result = th.xmlEscape('&');
+      assert.strictEqual(result, '&amp;');
+    });
+  }); // xmlEscape
+
 });
index f257cbfeeb0728af0b9b1bd135e49e7bb505d4c2..8ef104547c5907ff7c5dfe093e04d360d33895fd 100644 (file)
@@ -38,6 +38,7 @@ const stubFns = [
   'topicGetByUrl',
   'topicGetContentById',
   'topicPendingDelete',
+  'topicPublishHistory',
   'topicSet',
   'topicSetContent',
   'topicUpdate',