mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
docs(plugins): plugin wiki + reference plugin (M7)
- Wiki pages (sync to the GitHub wiki on push to main): Plugins overview + trust model, Plugin Development (SDK, definePlugin, ctx, routes/jobs, the client bridge, testing with the mock host), Plugin Permissions reference, and Publishing (registry PR + CI gates + provenance). Linked from the sidebar. - Reference plugin plugin-sdk/examples/trip-countdown: a complete, minimal- permission widget (reads trip data through ctx, renders in the sandboxed iframe via the bridge, filled-in README). Validated in the SDK test suite so it passes the exact gate authors face.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# Trip Countdown
|
||||
|
||||
> A dashboard widget that counts down the days to your next trip.
|
||||
|
||||

|
||||
|
||||
## What it does
|
||||
|
||||
Trip Countdown adds a small dashboard widget showing how many days remain until
|
||||
your upcoming trip starts. When a trip is under way it shows "in progress"
|
||||
instead. It's the reference plugin for the TREK plugin system — a minimal but
|
||||
complete widget that reads trip data through the sandbox and renders it in an
|
||||
isolated iframe.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||
## Permissions
|
||||
|
||||
| Permission | Why this plugin needs it |
|
||||
|---|---|
|
||||
| `db:read:trips` | To read the current trip's start date so it can compute the countdown. The read is membership-checked by TREK, so the widget only ever sees a trip you already have access to. |
|
||||
|
||||
It requests **no** network access and stores **no** data of its own.
|
||||
|
||||
## Setup
|
||||
|
||||
None. Enable plugins on your instance (`TREK_PLUGINS_ENABLED=true`), install and
|
||||
activate Trip Countdown, and the widget appears on your dashboard.
|
||||
|
||||
## Building
|
||||
|
||||
The widget ships plain JS/HTML — no build step. `server/index.js` is the plugin
|
||||
entry; `client/index.html` is the widget iframe.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see the TREK repository.
|
||||
@@ -0,0 +1,62 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Trip Countdown</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body { margin: 0; font: 14px system-ui, sans-serif; display: grid; place-items: center; height: 100vh;
|
||||
background: transparent; color: #111827; }
|
||||
body[data-theme="dark"] { color: #e5e7eb; }
|
||||
.card { text-align: center; padding: 16px; }
|
||||
.days { font-size: 40px; font-weight: 800; letter-spacing: -0.02em; }
|
||||
.label { font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.6; margin-top: 2px; }
|
||||
.name { margin-top: 6px; opacity: 0.85; }
|
||||
.empty { opacity: 0.6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card" id="root"><span class="empty">Loading…</span></div>
|
||||
<script>
|
||||
var ctx = null
|
||||
var reqSeq = 0
|
||||
|
||||
function send(msg) { window.parent.postMessage(msg, '*') }
|
||||
|
||||
function render(data) {
|
||||
var root = document.getElementById('root')
|
||||
if (!data || !data.trip) { root.innerHTML = '<span class="empty">No upcoming trip</span>'; return }
|
||||
var d = data.trip.days
|
||||
root.innerHTML = d == null
|
||||
? '<div class="name">' + escapeHtml(data.trip.name) + '</div><div class="label">in progress</div>'
|
||||
: '<div class="days">' + d + '</div><div class="label">days to go</div><div class="name">' + escapeHtml(data.trip.name) + '</div>'
|
||||
}
|
||||
|
||||
function escapeHtml(s) { return String(s).replace(/[&<>"]/g, function (c) {
|
||||
return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] }) }
|
||||
|
||||
function load() {
|
||||
var id = 'r' + (++reqSeq)
|
||||
send({ type: 'trek:invoke', requestId: id, sub: '/next?tripId=' + (ctx && ctx.tripId ? ctx.tripId : ''), method: 'GET' })
|
||||
window._pending = id
|
||||
}
|
||||
|
||||
window.addEventListener('message', function (e) {
|
||||
var m = e.data
|
||||
if (!m || typeof m !== 'object') return
|
||||
if (m.type === 'trek:context') {
|
||||
ctx = m
|
||||
document.body.dataset.theme = m.theme
|
||||
load()
|
||||
} else if (m.type === 'trek:response' && m.requestId === window._pending) {
|
||||
render(m.data)
|
||||
} else if (m.type === 'trek:error' && m.requestId === window._pending) {
|
||||
document.getElementById('root').innerHTML = '<span class="empty">Unavailable</span>'
|
||||
}
|
||||
})
|
||||
|
||||
// Ask TREK for context (theme, current trip id) — it replies with trek:context.
|
||||
send({ type: 'trek:ready' })
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,43 @@
|
||||
// Trip Countdown — reference widget plugin.
|
||||
//
|
||||
// Demonstrates the full author path: definePlugin, a permission-gated route that
|
||||
// reads trip data through ctx, and a response the client iframe renders. Runs
|
||||
// isolated; ctx is the only way to reach TREK.
|
||||
const { definePlugin } = require('trek-plugin-sdk')
|
||||
|
||||
/** Days (integer, >= 0) between now and an ISO date, or null if past/absent. */
|
||||
function daysUntil(iso) {
|
||||
if (!iso) return null
|
||||
const then = new Date(iso).getTime()
|
||||
if (Number.isNaN(then)) return null
|
||||
const diff = Math.ceil((then - Date.now()) / 86_400_000)
|
||||
return diff >= 0 ? diff : null
|
||||
}
|
||||
|
||||
module.exports = definePlugin({
|
||||
routes: [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/next',
|
||||
auth: true,
|
||||
async handler(req, ctx) {
|
||||
// The client passes the dashboard's current trip id; the host enforces that
|
||||
// req.user actually has access to it, so a plugin can't read foreign trips.
|
||||
const tripId = Number(req.query.tripId)
|
||||
if (!tripId) return { status: 200, headers: json(), body: JSON.stringify({ trip: null }) }
|
||||
|
||||
const t = await ctx.trips.getById(tripId, req.user.id)
|
||||
const days = daysUntil(t && (t.start_date || t.startDate))
|
||||
return {
|
||||
status: 200,
|
||||
headers: json(),
|
||||
body: JSON.stringify({ trip: t ? { name: t.name, days } : null }),
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
function json() {
|
||||
return { 'content-type': 'application/json' }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"id": "trip-countdown",
|
||||
"name": "Trip Countdown",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": 1,
|
||||
"author": "TREK",
|
||||
"description": "A dashboard widget counting down the days to your next trip.",
|
||||
"homepage": "https://github.com/mauriceboe/TREK",
|
||||
"license": "MIT",
|
||||
"icon": "CalendarClock",
|
||||
"type": "widget",
|
||||
"trek": ">=3.2.0 <4.0.0",
|
||||
"nativeModules": false,
|
||||
"permissions": ["db:read:trips"],
|
||||
"capabilities": {
|
||||
"widget": { "title": "Trip Countdown", "defaultSize": "small" }
|
||||
},
|
||||
"routes": [
|
||||
{ "method": "GET", "path": "/next", "auth": true }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user