Files
plainpages/plugins/scheduling/README.md
2026-06-20 00:42:23 +02:00

3.0 KiB

Scheduling — the reference plugin

A worked example of the plugin contract. Copy this folder, rename it (the folder name becomes the plugin id and mount path), and point it at your own backend.

What it demonstrates:

  • A list page that fetches upstream dataGET /scheduling/shifts calls the upstream REST service and renders the rows with the core building blocks (shifts.ejs → app shell, filter-bar, data-table). Search round-trips the URL; zero-JS. (It fetches all rows for brevity — for a large list, parse page/pageSize from parseListQuery, forward them upstream as a ?limit/ ?offset, and render pagination.ejs with paginate(), exactly as the built-in admin screens do.)
  • A form that forwards a write upstreamGET /scheduling/shifts/new renders the form, POST /scheduling/shifts CSRF-verifies it (ctx.verifyCsrf) and forwards the create upstream, then POST-redirect-GET. The form body lives in the plugin's own views/partials/shift-form.ejs, reusing the core field partial.
  • Permission-gated nav — the "Shifts" nav leaf and routes are gated on scheduling:read / scheduling:write; the whole "Scheduling" section is invisible to anyone without the grant.

The plugin holds no state — data lives upstream (README → Stateless). Handlers are thin and fetch is injectable, so they unit-test as pure functions (shifts.test.ts).

Upstream

Set SCHEDULING_UPSTREAM to your backend's base URL. The dev compose points it at a tiny in-memory mock (examples/shifts-upstream/) so docker compose up shows the plugin working out of the box. A malformed/non-http URL fails the boot loudly (the plugin's onBoot hook).

Upstream contract

Your backend must expose two routes; the plugin treats any non-2xx as a recoverable failure (the list degrades to a "try again" alert, the create re-renders the form keeping the input).

Route Request Success Response body
GET /shifts Accept: application/json 200 JSON array of { id, title, assignee, start, end } (all strings; missing fields coerce to "")
POST /shifts JSON body { title, assignee, start, end } 2xx ignored (the plugin POST-redirect-GETs back to the list)

Domain rules (overlap, capacity, time ordering) live in your backend — reject with a 4xx and the form re-renders. The plugin only validates that title and assignee are non-empty.

start/end come from the form's datetime-local inputs as YYYY-MM-DDTHH:mm and are stored and shown verbatim (the dev mock seeds a space-separated style, so created vs seeded rows differ only cosmetically) — normalise to your backend's format there if it matters.

Granting access

A user sees Scheduling once they hold the scheduling:read role in Keto (and scheduling:write to create). The one-command bootstrap grants both to the demo admin, so the seeded admin@plainpages.local can use it immediately.