Files
TREK/plugin-sdk/examples/koffi/server/index.js
T
mauriceboe 17f3c86967 feat(plugins): hero widget slot + Koffi reference plugin
Widget plugins can now declare capabilities.widget.slot 'hero' to render as a
transparent, click-through overlay sitting on the boarding-pass bar's top edge
(migration 157 persists capabilities; manifest validation server+SDK, feed
exposes the slot, dashboard mounts hero frames above the pass). Sidebar stays
the default slot.

Replaces the trip-countdown example with Koffi, the TREK mascot: an animated
suitcase with a 14-state behavior engine driven by real trip data — walking,
waving, napping, trolley rolls, passport-stamp stickers, a split-flap luggage-
tag countdown under 7 days, and sunglasses while the trip runs. Validated by
the SDK suite like any author plugin; published as mauriceboe/trek-plugin-koffi
in the registry.

Migrations 156/157 follow the idempotent ALTER pattern (the reconciliation
test re-runs everything from v135, so duplicate-column must stay non-fatal).
2026-07-04 19:11:23 +02:00

43 lines
1.6 KiB
JavaScript

// Koffi — the TREK suitcase mascot (hero-slot widget).
//
// One route: GET /state?tripId= → { days, ongoing, title }. The mascot's mood is
// driven by how close the trip is. The host membership-checks the trip against
// the acting user, so Trekki can only ever see a trip that user can see.
const DAY_MS = 86400000;
function midnight(iso) {
const d = new Date(iso + 'T00:00:00');
return Number.isNaN(d.getTime()) ? null : d.getTime();
}
module.exports = {
routes: [
{
method: 'GET',
path: '/state',
auth: true,
async handler(req, ctx) {
const json = { 'content-type': 'application/json' };
const tripId = Number(req.query.tripId);
if (!tripId) return { status: 200, headers: json, body: JSON.stringify({ days: null, ongoing: false, title: null }) };
const t = await ctx.trips.getById(tripId, req.user.id);
if (!t) return { status: 200, headers: json, body: JSON.stringify({ days: null, ongoing: false, title: null }) };
const now = new Date();
now.setHours(0, 0, 0, 0);
const start = t.start_date ? midnight(t.start_date) : null;
const end = t.end_date ? midnight(t.end_date) : null;
const ongoing = start !== null && start <= now.getTime() && (end === null || now.getTime() <= end);
const days = !ongoing && start !== null && start >= now.getTime()
? Math.round((start - now.getTime()) / DAY_MS)
: null;
return { status: 200, headers: json, body: JSON.stringify({ days, ongoing, title: t.title ?? null }) };
},
},
],
};