Skip to content

Commit 8ad4cf2

Browse files
Merge pull request #31 from HarperFast/feat/admin-ui
feat(plugin): management API + UI (Harper-auth) + cluster-wide queue control; v0.8.0
2 parents 9dd34e4 + bfe5a4a commit 8ad4cf2

17 files changed

Lines changed: 1907 additions & 51 deletions

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/plugin/README.md

Lines changed: 101 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ crawlers. It provides:
1010
back to.
1111
- Sitemap ingestion (`Sitemap`) that discovers URLs and schedules them for rendering.
1212
- A prerendered-page cache (`PrerenderedPage`) and indexability signals (`NonIndexable`).
13+
- A management API + UI at `/prerender_admin` (see [Management UI](#management-ui-prerender_admin)),
14+
authenticated with Harper users and restricted to `super_user`.
1315

1416
Everything that used to be hardcoded — domains, security token, device types, render/refresh
1517
schedules, user-agent strings, TTLs — is supplied per deployment through the host application's
@@ -91,6 +93,10 @@ rest: true # required for the @export-ed table REST endpoints
9193
jobLeaseTime: 600000 # 10m — how long a claimed job is leased
9294
statusSyncInterval: 60000 # 1m — how often queue status is recomputed/broadcast
9395

96+
management: # the admin API + UI at /prerender_admin
97+
enabled: true # false makes every management route 404
98+
scanCap: 20000 # ceiling on rows an overview scan walks (see "Management UI")
99+
94100
userAgents: # per-device User-Agent strings sent to the origin
95101
desktop: 'Mozilla/5.0 ... HarperPrerender/1.0'
96102
mobile: 'Mozilla/5.0 ... HarperPrerender/1.0'
@@ -166,14 +172,14 @@ Database/table names are fixed. Tables are split across databases by write-trans
166172
Harper serializes writes per database and commits each database independently, so the hot, high-write
167173
queue table is isolated and bursty/heavy writes don't serialize against it:
168174

169-
| Database | Tables | Notes |
170-
| ----------------- | ----------------------------- | ---------------------------------------------- |
171-
| `render_schedule` | `RenderSchedule` | the hot render queue — isolated |
172-
| `render_service` | `RenderTarget`, `QueueStatus` | render-target registry + per-host queue status |
173-
| `page_cache` | `PrerenderedPage` | rendered-HTML cache (heavy blob writes) |
174-
| `sitemaps` | `Sitemap`, `SitemapRefresh` | sitemap data + refresh marker |
175-
| `signals` | `NonIndexable` | indexability signals |
176-
| `coordination` | `SharedBuffer` | node-local cross-worker SAB (never replicated) |
175+
| Database | Tables | Notes |
176+
| ----------------- | --------------------------------------------- | ------------------------------------------------ |
177+
| `render_schedule` | `RenderSchedule` | the hot render queue — isolated |
178+
| `render_service` | `RenderTarget`, `QueueStatus`, `QueueControl` | target registry, observed status, desired status |
179+
| `page_cache` | `PrerenderedPage` | rendered-HTML cache (heavy blob writes) |
180+
| `sitemaps` | `Sitemap`, `SitemapRefresh` | sitemap data + refresh marker |
181+
| `signals` | `NonIndexable` | indexability signals |
182+
| `coordination` | `SharedBuffer` | node-local cross-worker SAB (never replicated) |
177183

178184
Because `RenderTarget` and `RenderSchedule` now live in separate databases, a target and its schedule
179185
are written as two independent commits (target first). The brief window where a target exists without a
@@ -186,14 +192,98 @@ See [`src/schemas/schema.graphql`](src/schemas/schema.graphql).
186192
| Method & path | Purpose |
187193
| -------------------------------------------- | ------------------------------------------------------------------- |
188194
| `GET /p/<absolute-url>` | Serve prerendered/cached HTML for a bot (cache hit or origin fetch) |
189-
| `POST /render_queue/pause` | Pause the queue |
190-
| `POST /render_queue/resume` | Resume the queue |
195+
| `POST /render_queue/pause` | Pause **this node's** queue |
196+
| `POST /render_queue/resume` | Clear this node's pause override |
191197
| `POST /render_queue/claim` | Claim due render jobs (`{ "limit": N }`) |
192198
| `POST /render_queue/job_result` | Submit a render result (binary; `x-metadata-size` header) |
193199
| `GET/PUT/DELETE /RenderTarget/...` | Manage render targets |
194200
| `POST /RenderTarget` `{action:"revalidate"}` | Force re-render of matching targets |
195201
| `GET/POST/DELETE /sitemaps/<url>` | Ingest / list / remove sitemaps |
196-
| `GET /queue_status` | Read per-host queue status |
202+
| `GET /queue_status` | Read per-node queue status (**observed**) |
203+
| `GET /queue_control` | Read the desired pause state (**intent**) |
204+
| `GET /prerender_admin` | Management UI + API — see below |
205+
206+
## Management UI (`/prerender_admin`)
207+
208+
A single self-contained page (no build step, no external requests) plus the JSON API behind
209+
it. Open `https://<host>:<port>/prerender_admin` and sign in with a Harper username and
210+
password.
211+
212+
**Authentication is Harper's own.** `POST /prerender_admin/login` calls Harper's
213+
`context.login()`, which authenticates against Harper users and sets the `hdb-session`
214+
cookie; every data and action route then requires `role.permission.super_user`. There is no
215+
separate password to configure. Two consequences worth knowing:
216+
217+
- The instance needs `authentication.enableSessions: true` (Harper's default). The UI says so
218+
explicitly if sessions are off rather than failing obscurely.
219+
- With `authentication.authorizeLocal: true` (also the default) requests from `127.0.0.1` are
220+
auto-authorized as super-user — so on a local instance the UI opens without a login. Set it
221+
to `false` if that matters to you.
222+
223+
The super-user check is written out on every route rather than relying on Harper's
224+
`allowRead`/`allowCreate` hooks, because those only run when `loadAsInstance !== false` — and
225+
this plugin's resources all set `loadAsInstance = false`.
226+
227+
| Method & path | Purpose | Gate |
228+
| ------------------------------- | --------------------------------------- | ------------ |
229+
| `GET /prerender_admin` | the UI page (contains no data) | public |
230+
| `GET /prerender_admin/session` | who am I | public |
231+
| `POST /prerender_admin/login` | `{ username, password }` | public |
232+
| `POST /prerender_admin/logout` | end the session | session |
233+
| `GET /prerender_admin/overview` | nodes, counts, backlog shape | `super_user` |
234+
| `GET /prerender_admin/config` | effective config + warnings | `super_user` |
235+
| `POST /prerender_admin/explain` | `{ url, deviceType }` → cache-key trace | `super_user` |
236+
| `POST /prerender_admin/queue` | `{ scope, paused }` → pause control | `super_user` |
237+
238+
### What it shows
239+
240+
- **Overview** — per-node queue status with staleness, table counts, the due-now backlog, and
241+
a next-24h histogram of `nextRenderTime`. That histogram is the quickest way to tell a
242+
healthy jittered spread from a render herd: a flat distribution means the initial-render
243+
jitter is working, a single tall bar means everything comes due at once.
244+
- **URL explainer** — paste a URL and see the ingress route that matched, the query allowlist
245+
it selected, the canonical URL, the resulting cache key, and the live
246+
`RenderTarget`/`RenderSchedule`/`PrerenderedPage`/`NonIndexable` rows under it. It also
247+
reports the key the URL would get under the global `url.queryParams`, and flags a
248+
difference — that divergence is the usual fingerprint of a permanent cache miss caused by a
249+
missing or misordered route. It surfaces a `NonIndexable` suppression too, which otherwise
250+
removes a URL from rotation silently.
251+
- **Config** — the effective merge of defaults and host overrides, with secrets shown only as
252+
whether they are set, alongside the risky-config warnings that previously existed only as
253+
startup log lines (empty security token, staging passthrough enabled, `renderNow` without a
254+
token).
255+
256+
### Counting is capped, on purpose
257+
258+
Table totals come from Harper's `getRecordCount()`, which is time-bounded and switches to
259+
sampling on a large table — it is reported with its `estimatedRange` rather than as an exact
260+
figure. The backlog/histogram scan walks at most `management.scanCap` rows (default 20 000)
261+
and marks the result `truncated` when it hits that ceiling. At 1M+ targets an exact range
262+
count is not a page-load query, so the UI labels an estimate as an estimate instead of
263+
presenting a short count as the total.
264+
265+
### Queue control: intent vs. observed
266+
267+
`claim` reads a **node-local**, non-replicated flag (a `SharedBuffer` SAB), which is why
268+
pausing used to mean calling `POST /render_queue/pause` on every node in turn. The
269+
`QueueControl` table now holds the _desired_ state and **is** replicated:
270+
271+
| Scope | Meaning |
272+
| ------------ | ------------------------------------------------------- |
273+
| `all` | cluster-wide default |
274+
| `<hostname>` | per-node override — wins over `all`, in both directions |
275+
276+
`paused: true` pauses, `paused: false` explicitly keeps a node running _through_ a
277+
cluster-wide pause, and deleting a node's row (`paused: null`) returns it to inheriting `all`.
278+
Each node resolves the intent on its own `queue.statusSyncInterval` tick, so **a change
279+
reaches a remote node within one interval (default 1m), not instantly** — the UI states this.
280+
`QueueStatus` remains what each node last _observed_; the UI shows both, and marks a node
281+
stale when it stops reporting.
282+
283+
`POST /render_queue/pause` stays deliberately node-scoped: that endpoint sets
284+
`loadAsInstance = false` and therefore enforces no authentication of its own, so it must not
285+
be able to stop the whole fleet. Cluster-scoped control is only reachable through the
286+
super-user-gated admin route.
197287

198288
## How it fits together
199289

packages/plugin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@harperfast/prerender",
3-
"version": "0.7.0",
3+
"version": "0.8.0",
44
"type": "module",
55
"description": "Configurable Harper plugin for prerendering pages for bots and crawlers",
66
"license": "Apache-2.0",

0 commit comments

Comments
 (0)