pco-plan: note categories as switches, top-bar variables as chips, no text-size field
Item notes get their own gear group, after Colors, with the existing
showNotes switch as the group's on/off in its title row. Its categories
come from Planning Center (GET /note-categories → the service type's
item_note_categories, by name, cached 10 min, an empty list rather than
an error when unconfigured or unreachable) and render as switches, three
across, all on until someone flips one; the tile reads an unset value as
every category, an array as exactly those, and a layout's old
comma-separated string as that list (blank = all).
The top bar template is a `template` field: one chip per placeholder
({series} {part} {serviceType} {date} {shortDate} {time} {length}
{items}) to drag or click into the text, with the help down to the
joining rules. Rendering is unchanged.
Text size leaves the gear window: the title bar's A− / A+ already size
the text and save it with the tile; the client falls back to 14 px when
nothing is saved.
tools/pco-mock.js serves item_note_categories (the categories its notes
use plus "Stage", which none does yet) so the route can be exercised.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
7a44b25d4b
commit
0c4828eb26
@@ -15,6 +15,8 @@
|
||||
* GET /plans?serviceTypeId=… → { options } for the admin "Plan" select
|
||||
* GET /documents → { options } for the admin start / end document selects
|
||||
* (documents ProPresenter has shown or lists — see below)
|
||||
* GET /note-categories → { options } for a tile's "Item notes" categories:
|
||||
* the service type's item note categories, by name
|
||||
* GET /timers → snapshot for the Timers module (guide: "Offering
|
||||
* GET /timers/stream → SSE, `timers` events timers to the Timers module")
|
||||
*
|
||||
@@ -26,6 +28,7 @@
|
||||
* GET /service_types/{st}/plans/{id} (title, dates…)
|
||||
* GET …/plans/{id}/plan_times (service start times)
|
||||
* GET …/plans/{id}/items?include=song,arrangement,item_notes
|
||||
* GET /service_types/{st}/item_note_categories (note category names)
|
||||
*
|
||||
* Following ProPresenter: instead of talking to ProPresenter a second time,
|
||||
* this module reads the propresenter-now-next module's own SSE feed over
|
||||
@@ -78,6 +81,8 @@ const NOW_NEXT_RETRY_MS = 3000;
|
||||
const NOW_NEXT_MISSING_RETRY_MS = 15000;
|
||||
/** How many distinct documents seen live are remembered for the admin selects. */
|
||||
const SEEN_DOCUMENTS_MAX = 200;
|
||||
/** How long a service type's item note categories are believed before Planning Center is asked again. */
|
||||
const NOTE_CATEGORIES_TTL_MS = 10 * MINUTE;
|
||||
/**
|
||||
* Service timer tone (the Timers module colours by it): running behind the
|
||||
* plan — completed items over their planned length, plus the current item's
|
||||
@@ -591,6 +596,44 @@ module.exports = {
|
||||
syncServiceSafe(Date.now(), null);
|
||||
}
|
||||
|
||||
/* — item note categories (a tile's "Item notes" → Categories switches) — */
|
||||
|
||||
let noteCategories = { at: 0, options: [] }; // the last good answer from Planning Center
|
||||
let noteCategoriesInFlight = null;
|
||||
|
||||
/**
|
||||
* The service type's item note categories, by name — notes are matched
|
||||
* to categories by name, so the names are the values. Believed for
|
||||
* NOTE_CATEGORIES_TTL_MS, read once however many tiles ask at the same
|
||||
* moment; a failed re-read keeps serving the last good list. A module
|
||||
* that cannot ask (no credentials, no service type) answers an empty
|
||||
* list — the gear window then says there is nothing to choose from yet.
|
||||
*/
|
||||
async function noteCategoryOptions() {
|
||||
if (!pco.configured || !serviceTypeId) return [];
|
||||
if (Date.now() - noteCategories.at < NOTE_CATEGORIES_TTL_MS) return noteCategories.options;
|
||||
if (!noteCategoriesInFlight) {
|
||||
noteCategoriesInFlight = (async () => {
|
||||
const body = await pco.getAll(`${SERVICES}/service_types/${encodeURIComponent(serviceTypeId)}/item_note_categories`);
|
||||
const seen = new Set();
|
||||
const options = (body.data || [])
|
||||
.filter((c) => !c?.attributes?.deleted_at)
|
||||
.map((c) => ({ name: String(c.attributes?.name || '').trim(), sequence: Number(c.attributes?.sequence) || 0 }))
|
||||
.sort((a, b) => a.sequence - b.sequence)
|
||||
.filter((c) => c.name && !seen.has(c.name.toLowerCase()) && seen.add(c.name.toLowerCase()))
|
||||
.map((c) => ({ value: c.name, label: c.name }));
|
||||
noteCategories = { at: Date.now(), options };
|
||||
return options;
|
||||
})().finally(() => { noteCategoriesInFlight = null; });
|
||||
}
|
||||
try {
|
||||
return await noteCategoriesInFlight;
|
||||
} catch (err) {
|
||||
log(`item note categories read failed: ${err?.message || err}`);
|
||||
return noteCategories.options;
|
||||
}
|
||||
}
|
||||
|
||||
function schedulePoll(ms) {
|
||||
if (stopped) return;
|
||||
clearTimeout(pollTimer);
|
||||
@@ -1214,6 +1257,7 @@ module.exports = {
|
||||
pickPlan,
|
||||
timersSnapshot,
|
||||
documentOptions,
|
||||
noteCategoryOptions,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -1312,6 +1356,23 @@ module.exports = {
|
||||
json(res, 200, current.documentOptions());
|
||||
},
|
||||
|
||||
/**
|
||||
* Options for a tile's "Item notes" → Categories switches: the
|
||||
* configured service type's item note categories, by name. Always a
|
||||
* 200 with a list — empty while the module is unconfigured, not
|
||||
* running or cannot reach Planning Center — because the gear window
|
||||
* has nothing better to do with an error than show an empty list.
|
||||
*/
|
||||
'GET /note-categories': async (req, res) => {
|
||||
let options = [];
|
||||
try {
|
||||
if (current) options = await current.noteCategoryOptions();
|
||||
} catch (err) {
|
||||
log(`note categories: ${err?.message || err}`);
|
||||
}
|
||||
json(res, 200, { options });
|
||||
},
|
||||
|
||||
/**
|
||||
* Options for the admin page's "Service type" select: every service
|
||||
* type, grouped by its Planning Center folder path — so the folders
|
||||
|
||||
Reference in New Issue
Block a user