Compare commits

..
Author SHA1 Message Date
Waters ChurchandClaude Opus 5 ea8be2ac76 Admin PIN: the fields say enough
The paragraph under the PIN fields explained what the PIN covers and how to
clear it by hand — and only someone already past the PIN could read it, so
the recovery note was never where it was needed. Gone, with the dead style
and the element lookup that hung off it.

The live line under the heading stays: it is the only thing that says
whether admin is locked, and the only warning that a passcode in the
server's environment outranks whatever is set here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 13:05:53 -04:00
12 changed files with 94 additions and 433 deletions
-6
View File
@@ -32,12 +32,6 @@ title-bar controls). The rules that matter most:
and bump the module's `version` on every change.
- Browsers never call upstream services directly; the module's server routes
proxy everything (no CORS, secrets stay server-side).
- **Keep the interface clean** (guide rules 5-11): no `help` and no label
over three words in a tile's gear; server-side settings in `configSchema`
only; a tile setting changes that tile alone; hide-toggles ungrouped when
there are four or fewer, in a folded **Show** group when there are five or
more; title-bar controls are icons with tooltips, never labels, and never
repeat something the gear already offers.
- Style with the shell's CSS variables only, scoped under
`[data-module="<id>"]`; survive upstream loss (reconnect + `setStatus`).
- Verify against the guide's testing section and resilience checklist. Mocks
+16 -62
View File
@@ -34,46 +34,6 @@ people running the booth depend on them.
The admin page refuses to install a module whose requirement the running
shell doesn't meet, and it offers updates by comparing versions.
### Keeping the interface clean
A dashboard is read across a booth, at a glance, by someone doing three
other things. These rules are what keep every tile looking like one product
instead of six. They are as binding as the four above.
5. **No explanatory text in a tile's gear.** No `help` on an
`instanceSchema` field or an `instanceGroups` entry. Labels are **three
words at most, one or two preferred** — "Slide count", "Item tags",
"Running clock". A control that cannot be understood in three words is
not a display choice; it belongs in `configSchema`, where `help` is
welcome and expected. *One exception:* a `template` field may keep a
single `help` line naming its placeholders, which nobody can guess.
6. **Server-side settings are admin-only.** Anything the module's server
acts on — a host, a credential, what to fetch, how often to poll — is
`configSchema`. `instanceSchema` is display choices and nothing else:
what this tile shows, and how big.
7. **A tile setting changes that tile and nothing else.** Never another
tile, never the module as a whole, never the server. If two tiles must
agree on something, it is either admin config or page state the shell
owns — not a setting each tile keeps its own copy of.
8. **Offer toggles for the elements you draw.** Every tile should let the
operator switch off the parts it does not need. **One to four toggles:**
give them no `group` at all, so they sit unlabelled at the top of the
gear. **Five or more:** one group named **Show**, `collapsed: true`,
`columns: 2`. Nothing else goes in that group — colours, formats and
text belong in groups of their own.
9. **Title-bar controls are icons.** No `label` on `addButton` or
`addMenu`; the tooltip carries the words, and `docs` above covers drawing
the icon. A small state indicator may ride alongside an icon — ProdCom's
channel button shows a count while a filter is on — but a control is
never named in the bar.
10. **If a control needs words to explain it, it is not a title-bar
control.** Put it in the gear. The bar is for actions whose icon and
tooltip say everything: refresh, clear, jump to latest, bigger text.
11. **One control per action.** Whatever has a dedicated title-bar button is
not offered again in the gear, and the reverse. Text size is A− / A+ up
there, never a "Text size" field; a timestamps button in the bar means
no timestamps switch below it.
## The shape of a module
A module is one folder dropped into `modules/`:
@@ -217,18 +177,14 @@ the group name:
`help` on a field puts one line of guidance under its control — allowed
placeholders, where a value comes from. Keep labels short and put the
explanation there. **This is for `configSchema` only**: a tile's gear
carries no explanations at all (rule 5), so a setting that needs one is
telling you it belongs in admin.
explanation there.
`group` works on `instanceSchema` fields too, and a manifest
`group` and `help` work on `instanceSchema` fields too, and a manifest
`instanceGroups` map plays the `configGroups` role for the tile's gear
popover (`collapsed: true | false`, `columns`, `toggle` — but not `help`,
per rule 5). That is how a module with a long show/hide checklist keeps the
popover usable: five or more hide-toggles go in one **Show** group, folded
by default, and fewer than five go in no group at all — see `pco-plan` for
the long form and `slack` for the short one. Display choices belong there,
per tile; admin config is for connections. The popover itself is a small window beside the tile — sized to
popover (`collapsed: true | false`, `columns`, `help`, `toggle`). That is how
a module with a long show/hide checklist keeps the popover usable — see
`pco-plan`. Display choices belong there, per tile; admin config is for
connections. The popover itself is a small window beside the tile — sized to
its content, moved by the slim bar on top, closed by clicking anywhere else
or pressing Escape — so a tile never has to be big enough to hold its own
settings, and a change can be watched as it lands. Under everything you
@@ -398,19 +354,17 @@ menu.setLabel('Channels (2/4) ▾'); // update the button text any time
```
Everything you add is removed automatically when your instance stops, so a
remount never duplicates controls. **Icons only, no labels** (rule 9): the
title bar is 32 px tall and shared with the tile's name, and the tooltip
says what the icon means. Draw icons as inline SVG rather than characters —
a glyph such as ✕ or + takes its size from whatever font each browser
picks; text-size buttons are a drawn − and +. A small state indicator may
ride alongside an icon (ProdCom's channel button carries a count while a
filter is on); a name may not.
remount never duplicates controls. Keep them compact (icon buttons, short
labels): the title bar is 32 px tall and shared with the tile's name. Draw
icons as inline SVG rather than characters — a glyph such as ✕ or + takes
its size from whatever font each browser picks; text-size buttons are a
drawn − and +.
**One control per action** (rule 11). Whatever has a dedicated header button
is not offered again in the gear popover: text size is − / + up here
(persisted through `saveInstanceSettings({ textSize })`, read back with a
sane fallback), never a "Text size" field in `instanceSchema`; a timestamps
or newest-first toggle in the header means no such switch in the gear. The
**One control per action.** Whatever has a dedicated header button is not
offered again in the gear popover: text size is A− / A+ up here (persisted
through `saveInstanceSettings({ textSize })`, read back with a sane
fallback), never a "Text size" field in `instanceSchema`; a timestamps or
newest-first toggle in the header means no such switch in the gear. The
gear is for display choices that have no button of their own.
One more thing to know: the whole title bar folds away when the dashboard
+49 -245
View File
@@ -1,23 +1,15 @@
{
"id": "pco-plan",
"name": "PCO Plan",
"version": "1.5.0",
"version": "1.4.0",
"proddash": ">=1.4.0",
"description": "Order of service from Planning Center Services",
"provides": [
"timers"
],
"provides": ["timers"],
"client": "client.js",
"server": "server.js",
"style": "style.css",
"minSize": {
"w": 3,
"h": 3
},
"defaultSize": {
"w": 5,
"h": 6
},
"minSize": { "w": 3, "h": 3 },
"defaultSize": { "w": 5, "h": 6 },
"configGroups": {
"Planning Center credentials": {
"collapsed": "whenSet",
@@ -56,12 +48,7 @@
"type": "select",
"label": "Plan",
"default": "",
"options": [
{
"value": "",
"label": "Automatic — today's plan, else the next upcoming"
}
],
"options": [{ "value": "", "label": "Automatic — today's plan, else the next upcoming" }],
"optionsRoute": "/plans",
"optionsDependsOn": "serviceTypeId"
},
@@ -95,14 +82,8 @@
"group": "Service timing",
"showWhen": "useDocumentTriggers",
"options": [
{
"value": "schedule",
"label": "Scheduled service time"
},
{
"value": "document",
"label": "ProPresenter document"
}
{ "value": "schedule", "label": "Scheduled service time" },
{ "value": "document", "label": "ProPresenter document" }
]
},
"serviceStartDocument": {
@@ -143,94 +124,37 @@
"Item notes": {
"toggle": "showNotes"
},
"Show": {
"Show / hide": {
"collapsed": true,
"columns": 2
"columns": 3,
"help": "Switch off anything this tile should not show."
}
},
"instanceSchema": {
"autoScroll": {
"type": "boolean",
"label": "Auto-scroll",
"default": true
},
"autoScroll": { "type": "boolean", "label": "Keep the current item in view", "default": true },
"headerTemplate": {
"type": "template",
"label": "Top bar text",
"default": "{series} • {part} | {serviceType} • {date}",
"group": "Top bar",
"variables": [
{
"value": "{series}",
"label": "Series"
},
{
"value": "{part}",
"label": "Part / plan title"
},
{
"value": "{serviceType}",
"label": "Service type"
},
{
"value": "{date}",
"label": "Date"
},
{
"value": "{shortDate}",
"label": "Short date"
},
{
"value": "{time}",
"label": "Service time"
},
{
"value": "{length}",
"label": "Planned length"
},
{
"value": "{items}",
"label": "Item count"
}
{ "value": "{series}", "label": "Series" },
{ "value": "{part}", "label": "Part / plan title" },
{ "value": "{serviceType}", "label": "Service type" },
{ "value": "{date}", "label": "Date" },
{ "value": "{shortDate}", "label": "Short date" },
{ "value": "{time}", "label": "Service time" },
{ "value": "{length}", "label": "Planned length" },
{ "value": "{items}", "label": "Item count" }
],
"help": "Use • between items and | to start a second line."
},
"keyColor": {
"type": "color",
"label": "Song key",
"default": "#4ea1ff",
"group": "Colors"
},
"headerColor": {
"type": "color",
"label": "Section headers",
"default": "#2ee59a",
"group": "Colors"
},
"currentColor": {
"type": "color",
"label": "Current item",
"default": "#2ee59a",
"group": "Colors"
},
"headerGradient": {
"type": "boolean",
"label": "Header fade",
"default": true,
"group": "Colors"
},
"tintByType": {
"type": "boolean",
"label": "Tint by type",
"default": false,
"group": "Colors"
},
"showNotes": {
"type": "switch",
"label": "Item notes",
"default": true,
"group": "Item notes"
},
"keyColor": { "type": "color", "label": "Song key", "default": "#4ea1ff", "group": "Colors" },
"headerColor": { "type": "color", "label": "Section headers", "default": "#2ee59a", "group": "Colors" },
"currentColor": { "type": "color", "label": "Current item", "default": "#2ee59a", "group": "Colors" },
"headerGradient": { "type": "boolean", "label": "Fade header color to the right", "default": true, "group": "Colors" },
"tintByType": { "type": "boolean", "label": "Tint items by type (song / media / other)", "default": false, "group": "Colors" },
"showNotes": { "type": "switch", "label": "Item notes", "default": true, "group": "Item notes" },
"noteCategories": {
"type": "multiselect",
"label": "Categories",
@@ -239,149 +163,29 @@
"columns": 3,
"allByDefault": true
},
"showTopBar": {
"type": "boolean",
"label": "Top bar",
"default": true,
"group": "Show"
},
"showSeries": {
"type": "boolean",
"label": "Series name",
"default": true,
"group": "Show"
},
"showPart": {
"type": "boolean",
"label": "Part / title",
"default": true,
"group": "Show"
},
"showServiceType": {
"type": "boolean",
"label": "Service type",
"default": true,
"group": "Show"
},
"showDate": {
"type": "boolean",
"label": "Plan date",
"default": true,
"group": "Show"
},
"showServiceTime": {
"type": "boolean",
"label": "Service time",
"default": true,
"group": "Show"
},
"showTotalLength": {
"type": "boolean",
"label": "Total length",
"default": true,
"group": "Show"
},
"showRunningClock": {
"type": "boolean",
"label": "Running clock",
"default": true,
"group": "Show"
},
"showCountdown": {
"type": "boolean",
"label": "Countdown",
"default": true,
"group": "Show"
},
"showHeaders": {
"type": "boolean",
"label": "Section headers",
"default": true,
"group": "Show"
},
"showItemNumbers": {
"type": "boolean",
"label": "Item numbers",
"default": false,
"group": "Show"
},
"showStartTimes": {
"type": "boolean",
"label": "Start times",
"default": true,
"group": "Show"
},
"showLength": {
"type": "boolean",
"label": "Item length",
"default": true,
"group": "Show"
},
"showItemType": {
"type": "boolean",
"label": "Type badge",
"default": true,
"group": "Show"
},
"showKey": {
"type": "boolean",
"label": "Song key",
"default": true,
"group": "Show"
},
"showArrangement": {
"type": "boolean",
"label": "Arrangement",
"default": false,
"group": "Show"
},
"showSongAuthor": {
"type": "boolean",
"label": "Author / CCLI",
"default": false,
"group": "Show"
},
"showDescription": {
"type": "boolean",
"label": "Item description",
"default": true,
"group": "Show"
},
"showPreService": {
"type": "boolean",
"label": "Pre-service",
"default": true,
"group": "Show"
},
"showPostService": {
"type": "boolean",
"label": "Post-service",
"default": true,
"group": "Show"
},
"showNowIndicator": {
"type": "boolean",
"label": "Now indicator",
"default": true,
"group": "Show"
},
"showItemRuntime": {
"type": "boolean",
"label": "Item runtime",
"default": true,
"group": "Show"
},
"dimCompleted": {
"type": "boolean",
"label": "Dim completed",
"default": true,
"group": "Show"
},
"showLiveFooter": {
"type": "boolean",
"label": "Match line",
"default": true,
"group": "Show"
}
"showTopBar": { "type": "boolean", "label": "Top bar", "default": true, "group": "Show / hide" },
"showSeries": { "type": "boolean", "label": "Series name", "default": true, "group": "Show / hide" },
"showPart": { "type": "boolean", "label": "Part # / plan title", "default": true, "group": "Show / hide" },
"showServiceType": { "type": "boolean", "label": "Service type", "default": true, "group": "Show / hide" },
"showDate": { "type": "boolean", "label": "Plan date", "default": true, "group": "Show / hide" },
"showServiceTime": { "type": "boolean", "label": "Service time", "default": true, "group": "Show / hide" },
"showTotalLength": { "type": "boolean", "label": "Planned total length", "default": true, "group": "Show / hide" },
"showRunningClock": { "type": "boolean", "label": "Running clock (ahead / behind)", "default": true, "group": "Show / hide" },
"showCountdown": { "type": "boolean", "label": "Countdown to service start", "default": true, "group": "Show / hide" },
"showHeaders": { "type": "boolean", "label": "Section headers", "default": true, "group": "Show / hide" },
"showItemNumbers": { "type": "boolean", "label": "Item numbers", "default": false, "group": "Show / hide" },
"showStartTimes": { "type": "boolean", "label": "Scheduled start times", "default": true, "group": "Show / hide" },
"showLength": { "type": "boolean", "label": "Item length", "default": true, "group": "Show / hide" },
"showItemType": { "type": "boolean", "label": "Item type badge", "default": true, "group": "Show / hide" },
"showKey": { "type": "boolean", "label": "Song key", "default": true, "group": "Show / hide" },
"showArrangement": { "type": "boolean", "label": "Song arrangement", "default": false, "group": "Show / hide" },
"showSongAuthor": { "type": "boolean", "label": "Song author / CCLI", "default": false, "group": "Show / hide" },
"showDescription": { "type": "boolean", "label": "Item description", "default": true, "group": "Show / hide" },
"showPreService": { "type": "boolean", "label": "Pre-service items", "default": true, "group": "Show / hide" },
"showPostService": { "type": "boolean", "label": "Post-service items", "default": true, "group": "Show / hide" },
"showNowIndicator": { "type": "boolean", "label": "Now indicator", "default": true, "group": "Show / hide" },
"showItemRuntime": { "type": "boolean", "label": "Item runtime (elapsed vs planned)", "default": true, "group": "Show / hide" },
"dimCompleted": { "type": "boolean", "label": "Dim completed items", "default": true, "group": "Show / hide" },
"showLiveFooter": { "type": "boolean", "label": "ProPresenter match line", "default": true, "group": "Show / hide" }
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"id": "prodcom-transcript",
"name": "ProdCom",
"version": "1.3.2",
"version": "1.3.1",
"proddash": ">=1.1.0",
"description": "Live ProdCom transcript",
"permissions": [
@@ -40,7 +40,7 @@
"instanceSchema": {
"showAvatars": {
"type": "boolean",
"label": "Channel icons",
"label": "Show channel icons",
"default": true
}
}
+15 -74
View File
@@ -1,34 +1,22 @@
{
"id": "propresenter-now-next",
"name": "ProPresenter",
"version": "1.1.1",
"version": "1.1.0",
"proddash": ">=1.5.0",
"description": "What is live in ProPresenter and what comes next",
"permissions": [
{
"kind": "local-network",
"reason": "Reaching ProPresenter on this network"
}
{ "kind": "local-network", "reason": "Reaching ProPresenter on this network" }
],
"client": "client.js",
"server": "server.js",
"style": "style.css",
"minSize": {
"w": 3,
"h": 3
},
"defaultSize": {
"w": 4,
"h": 5
},
"minSize": { "w": 3, "h": 3 },
"defaultSize": { "w": 4, "h": 5 },
"configSchema": {
"propresenter": {
"type": "endpoint",
"label": "ProPresenter",
"default": {
"host": "",
"port": 1025
}
"default": { "host": "", "port": 1025 }
},
"password": {
"type": "password",
@@ -37,64 +25,17 @@
}
},
"instanceSchema": {
"cleanTitles": {
"type": "switch",
"label": "Tidy titles",
"default": true
},
"showPlaylist": {
"type": "switch",
"label": "Playlist",
"default": true,
"group": "Show"
},
"showLive": {
"type": "switch",
"label": "Live status",
"default": true,
"group": "Show"
},
"showNowLabel": {
"type": "switch",
"label": "Now label",
"default": true,
"group": "Show"
},
"showSlideCount": {
"type": "switch",
"label": "Slide count",
"default": true,
"group": "Show"
},
"showProgress": {
"type": "switch",
"label": "Progress bar",
"default": true,
"group": "Show"
},
"showNext": {
"type": "switch",
"label": "Next list",
"default": true,
"group": "Show"
},
"showNextLabel": {
"type": "switch",
"label": "Next label",
"default": true,
"group": "Show"
},
"showTags": {
"type": "switch",
"label": "Item tags",
"default": true,
"group": "Show"
}
"cleanTitles": { "type": "switch", "label": "Tidy titles", "default": true },
"showPlaylist": { "type": "switch", "label": "Playlist", "default": true, "group": "Show" },
"showLive": { "type": "switch", "label": "Live status", "default": true, "group": "Show" },
"showNowLabel": { "type": "switch", "label": "Now label", "default": true, "group": "Show" },
"showSlideCount": { "type": "switch", "label": "Slide count", "default": true, "group": "Show" },
"showProgress": { "type": "switch", "label": "Progress bar", "default": true, "group": "Show" },
"showNext": { "type": "switch", "label": "Next list", "default": true, "group": "Show" },
"showNextLabel": { "type": "switch", "label": "Next label", "default": true, "group": "Show" },
"showTags": { "type": "switch", "label": "Item tags", "default": true, "group": "Show" }
},
"instanceGroups": {
"Show": {
"collapsed": true,
"columns": 2
}
"Show": { "collapsed": false, "columns": 2 }
}
}
+1 -1
View File
@@ -140,7 +140,7 @@ module.exports = {
showSlideCount: { type: 'switch', label: 'Slide count', default: true, group: 'Show' },
showProgress: { type: 'switch', label: 'Progress bar', default: true, group: 'Show' },
},
instanceGroups: { Show: { collapsed: true, columns: 2 } },
instanceGroups: { Show: { collapsed: false, columns: 2 } },
},
];
},
+6 -18
View File
@@ -15,7 +15,7 @@
Per-tile choices: the gear popover's static switches (whole sources,
headings, status lines, the clock's format, the state colours) and the
timers header menu, which hides any single timer — keyed `source:id`
"Timers ▾" header menu, which hides any single timer — keyed `source:id`
and saved with the layout via saveInstanceSettings. The header can be
hidden by the user, so the static switches stay the primary controls.
A solo tile's gear carries only the settings its one card can use (the
@@ -196,10 +196,6 @@ function defaultStatus(timer, value, tone) {
/* ── the tile ─────────────────────────────────────────────────────────── */
/* The header menu's icon: a stopwatch, for the list of timers behind it.
Title-bar controls carry no words (module guide, rule 9). */
const TIMERS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 2h4"/><path d="M12 14V9"/><circle cx="12" cy="14" r="8"/></svg>';
export default function create({ root, moduleApi }) {
let state = null;
let feedOffline = false;
@@ -231,7 +227,7 @@ export default function create({ root, moduleApi }) {
}
/* Which picker entry this tile was added as (the module's tiles() list):
'' or 'all' = every timer, selectable via the timers menu;
'' or 'all' = every timer, selectable via the Timers ▾ menu;
'solo:<source>:<id>' = exactly one card, no menu, no headings. The
earlier entries 'timer:<uuid>' and 'ltc' map onto the new keys. */
const variant = String(moduleApi.variant || '');
@@ -287,8 +283,8 @@ export default function create({ root, moduleApi }) {
let menuEl = null; // the open menu's element (filled on each open)
const itemsMenu = solo ? null : moduleApi.header.addMenu({
icon: TIMERS_SVG,
title: 'Timers',
label: 'Timers ▾',
title: 'Choose timers',
build(menu) {
// the shell hands over an emptied element on every open
menu.classList.add('tm-menu');
@@ -296,12 +292,6 @@ export default function create({ root, moduleApi }) {
rebuildMenu(true);
},
});
// The icon carries the name; a count rides alongside it while some timers
// are hidden, so the bar says "filtered" without saying it in words.
const itemsCountEl = document.createElement('span');
itemsCountEl.className = 'tm-count';
itemsCountEl.hidden = true;
if (itemsMenu) itemsMenu.button.appendChild(itemsCountEl);
function menuRows() {
const rows = [];
@@ -329,9 +319,7 @@ export default function create({ root, moduleApi }) {
if (!itemsMenu) return;
const rows = menuRows();
const visible = rows.filter((r) => !hidden.has(r.key)).length;
const filtering = visible !== rows.length;
itemsCountEl.textContent = filtering ? `${visible}/${rows.length}` : '';
itemsCountEl.hidden = !filtering;
itemsMenu.setLabel(visible === rows.length ? 'Timers ▾' : `Timers (${visible}/${rows.length}) ▾`);
}
let menuSignature = '';
@@ -791,7 +779,7 @@ export default function create({ root, moduleApi }) {
let hint = '';
const groups = visibleGroups();
if (state && !state.sources.length) hint = 'Every source is switched off — turn one on in Admin.';
else if (state && !groups.length) hint = 'Nothing selected — switch a source on in this tile’s settings, or pick timers in the title bar’s timers menu.';
else if (state && !groups.length) hint = 'Nothing selected — switch a source on in this tile’s settings, or pick timers in the Timers ▾ menu.';
hintEl.hidden = !hint;
hintEl.textContent = hint;
groupsEl.hidden = Boolean(hint);
+4 -3
View File
@@ -1,7 +1,7 @@
{
"id": "propresenter-timers",
"name": "Timers",
"version": "1.4.0",
"version": "1.3.0",
"proddash": ">=1.4.0",
"description": "Timers, Clocks, and LTC From configured Modules",
"provides": [
@@ -112,7 +112,7 @@
},
"instanceGroups": {
"Show": {
"collapsed": true,
"collapsed": false,
"columns": 2
},
"Clock": {
@@ -121,7 +121,8 @@
},
"Colours": {
"collapsed": true,
"columns": 2
"columns": 2,
"help": "The time's colour per state. Left at the default, a colour follows the dashboard theme; one you change here wins over it."
}
},
"instanceSchema": {
+1 -9
View File
@@ -321,7 +321,7 @@
@keyframes tm-nosignal-blink { 50% { opacity: 0.35; } }
/* ── the timers menu (container styled/positioned by the shell) ── */
/* ── the "Timers ▾" menu (container styled/positioned by the shell) ── */
[data-module="propresenter-timers"] .tm-menu-item {
display: flex;
@@ -383,11 +383,3 @@
font-size: 12px;
white-space: nowrap;
}
/* the count beside the title-bar timers icon, while some are hidden */
[data-module="propresenter-timers"] .tm-count {
font-size: 10px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
[data-module="propresenter-timers"] .tm-count[hidden] { display: none; }
-8
View File
@@ -550,11 +550,3 @@ a.btn { text-decoration: none; }
gap: 8px;
margin-left: auto;
}
.pin-note { margin-top: 12px; }
.pin-note code {
font-size: 0.92em;
padding: 1px 5px;
border-radius: 4px;
background: var(--bg);
border: 1px solid var(--border);
}
-1
View File
@@ -132,7 +132,6 @@ function renderPin(state) {
: set
? 'Admin is locked. This page asks for the PIN, and so does every admin request.'
: 'Admin is open to anyone on the network. Set a 6-digit PIN to lock it.';
if (state.dataDir) document.getElementById('pin-file').textContent = state.dataDir + '/proddash.json';
}
function pinMessage(text, bad = false) {
-4
View File
@@ -92,10 +92,6 @@
<button class="btn" id="pin-clear" hidden>Turn off</button>
</div>
</div>
<p class="admin-copy pin-note">Only this page and the admin API ask for it — dashboards stay
open, and it is nothing to do with a module's own PIN, such as Slack's send PIN. Forgotten?
Remove <code>adminPin</code> from <code id="pin-file">proddash.json</code> in the settings
folder and restart.</p>
<div class="admin-status" id="pin-status"></div>
</div>
</section>