Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0be37796b | ||
|
|
6cca6d4d1a | ||
|
|
1be5cbef31 | ||
|
|
1c8cd63cd0 | ||
|
|
af6322d079 | ||
|
|
168f717398 | ||
|
|
67bcd3640a | ||
|
|
095fe66118 | ||
|
|
6417c3aa54 | ||
|
|
c1d9fa0527 |
@@ -100,10 +100,24 @@ with this machine's shell settings — the same keys as the checked-in
|
||||
| --- | --- |
|
||||
| `port` | Port ProdDash serves on (default `24500`). The macOS launcher writes this one, so the port it is set to holds however ProdDash is started. |
|
||||
| `adminPasscode` | If set, the admin page asks for this shared passcode. Leave `""` for none — it's a plain-HTTP LAN tool. |
|
||||
| `forge` | Where updates and module installs are fetched from. Absent or `"github"` uses GitHub; a Gitea or Forgejo base URL such as `"http://10.3.8.90:3000"` uses a server of your own. Set it per machine in the data directory, alongside `repo`. |
|
||||
| `theme` | The dashboard palette: `booth` (default), `harbor`, `graphite`, `ember` or `daylight`. The admin page's **Theme** section writes this one — see [Themes](#themes). |
|
||||
|
||||
**Updating from your own git server.** ProdDash fetches one `tar.gz` of the
|
||||
branch — nothing else — so any Gitea or Forgejo works. Point `forge` at its
|
||||
base URL and `repo` at the owner/repo there:
|
||||
|
||||
```json
|
||||
{ "forge": "http://10.3.8.90:3000", "repo": "michael/ProdDash", "branch": "main" }
|
||||
```
|
||||
|
||||
Gitea spells an archive URL differently from GitHub, which is the only thing
|
||||
that changes; the tarball itself is identical, commit hash included, so update
|
||||
detection and module installs behave exactly as before. Make the repository
|
||||
readable without signing in, or ProdDash cannot fetch it.
|
||||
|
||||
Environment variables override both files: `PORT`, `PRODDASH_PASSCODE`, and
|
||||
`PRODDASH_DATA_DIR` to put the data directory somewhere else (a relative path
|
||||
`PRODDASH_FORGE`, and `PRODDASH_DATA_DIR` to put the data directory somewhere else (a relative path
|
||||
is taken from the app folder; `PRODDASH_DATA_DIR=config` restores the old
|
||||
in-repo location). If the default directory can't be created, the server says
|
||||
so and falls back to the app's `config/` folder.
|
||||
@@ -131,10 +145,11 @@ set on a tile itself (a PCO Plan header colour, say) is deliberately kept.
|
||||
|
||||
## Installing modules and updating
|
||||
|
||||
Everything below happens in the **admin page**; ProdDash talks to its GitHub
|
||||
repository (`michaelmcgary2008/ProdDash`, branch `main` — change with
|
||||
`repo` / `branch` in `proddash.json` or the `PRODDASH_REPO` variable,
|
||||
`owner/repo#branch`). It downloads one small archive of the branch, at most
|
||||
Everything below happens in the **admin page**; ProdDash talks to its git
|
||||
repository — `michael/ProdDash` on the church's own Gitea at
|
||||
`chronicles-ab.waterschurch.org:3000`, branch `main`. Change it with `forge`,
|
||||
`repo` and `branch` in `proddash.json`, or the `PRODDASH_FORGE` and
|
||||
`PRODDASH_REPO` variables (`owner/repo#branch`). It downloads one small archive of the branch, at most
|
||||
every few minutes, only while the admin page is open or for the update check
|
||||
below. Nothing is installed without someone clicking.
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"port": 24500,
|
||||
"adminPasscode": "",
|
||||
"repo": "michaelmcgary2008/ProdDash",
|
||||
"forge": "http://chronicles-ab.waterschurch.org:3000",
|
||||
"repo": "michael/ProdDash",
|
||||
"branch": "main"
|
||||
}
|
||||
|
||||
+16
-5
@@ -141,13 +141,17 @@ const sendTarget = (() => {
|
||||
/**
|
||||
* Where a send goes, given the configured channels in admin order: the
|
||||
* selection while it is still configured and its transcript is on the
|
||||
* page; otherwise the first configured channel.
|
||||
* page, else the first configured channel that IS on this page — with one
|
||||
* transcript up, that one — and only if the page shows none, the first
|
||||
* configured channel. A send must never quietly go to a channel the
|
||||
* operator cannot see.
|
||||
*/
|
||||
resolve(configured) {
|
||||
const list = Array.isArray(configured) ? configured : [];
|
||||
if (!list.length) return '';
|
||||
if (id && list.includes(id) && this.hasTranscript(id)) return id;
|
||||
return list[0];
|
||||
const onPage = list.find((c) => this.hasTranscript(c));
|
||||
return onPage || list[0];
|
||||
},
|
||||
on(fn) {
|
||||
bus.addEventListener('change', fn);
|
||||
@@ -1100,7 +1104,7 @@ function createSendTile({ root, moduleApi, mode }) {
|
||||
if (stopped || !state) return;
|
||||
if (token) {
|
||||
if (composeEls) {
|
||||
composeEls.line.textContent = sendingLine();
|
||||
if (composeEls.line) composeEls.line.textContent = sendingLine();
|
||||
if (composeEls.input) composeEls.input.placeholder = placeholder();
|
||||
}
|
||||
} else renderGate();
|
||||
@@ -1177,14 +1181,21 @@ function createSendTile({ root, moduleApi, mode }) {
|
||||
let composeEls = null;
|
||||
function buildCompose() {
|
||||
compose.textContent = '';
|
||||
// Send message names the channel in the box's own placeholder and locks
|
||||
// from the title bar, so a line above the field would only say both
|
||||
// again. Quick replies has no placeholder — there it is the only place
|
||||
// the target is named, so it stays.
|
||||
let line = null;
|
||||
if (mode !== 'send') {
|
||||
const head = el('div', 'sl-compose-head');
|
||||
const line = el('span', 'sl-sending', sendingLine());
|
||||
line = el('span', 'sl-sending', sendingLine());
|
||||
const lockLink = el('button', 'sl-lock-link', 'Lock');
|
||||
lockLink.type = 'button';
|
||||
lockLink.title = 'Lock now';
|
||||
lockLink.addEventListener('click', () => lock('Locked'));
|
||||
head.append(line, lockLink);
|
||||
compose.appendChild(head);
|
||||
}
|
||||
|
||||
const foot = el('div', 'sl-compose-foot');
|
||||
const msg = el('div', 'sl-msg');
|
||||
@@ -1462,7 +1473,7 @@ function createSendTile({ root, moduleApi, mode }) {
|
||||
if (!s.sending?.enabled || !s.sending?.pinSet) return void lock('');
|
||||
if (!composeEls) return void showCompose(); // the page was unlocked before this tile appeared
|
||||
if (composeEls) {
|
||||
composeEls.line.textContent = sendingLine();
|
||||
if (composeEls.line) composeEls.line.textContent = sendingLine();
|
||||
if (composeEls.input) composeEls.input.placeholder = placeholder();
|
||||
}
|
||||
} else renderGate();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "slack",
|
||||
"name": "Slack",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"proddash": ">=1.4.0",
|
||||
"description": "Live transcripts of this location's Slack channels, plus PIN-protected sending as the location",
|
||||
"client": "client.js",
|
||||
@@ -21,7 +21,7 @@
|
||||
"help": "Create a Slack app in the workspace (api.slack.com/apps → Create New App → From scratch) and, under OAuth & Permissions → User Token Scopes, add channels:history, channels:read, groups:history, groups:read, chat:write, users:read, reactions:write (emoji reactions) and emoji:read (the workspace's custom emoji). Sign in to Slack as this location's account — the one named for the campus — open the app's OAuth & Permissions page and press Install to Workspace (or Reinstall to Workspace), then copy the User OAuth Token (xoxp-…) it shows and paste it here: messages from this machine are then literally from the location. A bot token (xoxb-…) works too; it posts as the app, with \"Send as\" naming the location."
|
||||
},
|
||||
"Channels": {
|
||||
"help": "Every channel picked here becomes its own transcript tile in the + picker, titled with the channel's name. The Send message and Quick replies tiles post to whichever transcript was clicked last — its bottom edge lights up in the accent colour — and to the first channel here until one is clicked. Only channels the account has joined are listed; join one in Slack first. Each channel is read on its own schedule, all of them together kept inside Slack's rate limit."
|
||||
"help": "Every channel picked here becomes its own transcript tile in the + picker, titled with the channel's name. The Send message and Quick replies tiles post to whichever transcript was clicked last — its bottom edge lights up in the accent colour — and until one is clicked, to the first channel here whose transcript is on that dashboard: with a single transcript up, that one. A send never goes to a channel the dashboard is not showing. Only channels the account has joined are listed; join one in Slack first. Each channel is read on its own schedule, all of them together kept inside Slack's rate limit."
|
||||
},
|
||||
"Sending": {
|
||||
"toggle": "sendEnabled",
|
||||
@@ -49,7 +49,7 @@
|
||||
"default": [],
|
||||
"group": "Channels",
|
||||
"optionsRoute": "/channels",
|
||||
"help": "One transcript tile per channel; the first one is where sends go until a transcript is clicked. Only channels the account has joined are listed — join one in Slack first."
|
||||
"help": "One transcript tile per channel; until a transcript is clicked, sends go to the first channel here that the dashboard is actually showing. Only channels the account has joined are listed — join one in Slack first."
|
||||
},
|
||||
"pollSeconds": {
|
||||
"type": "number",
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "proddash",
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.0",
|
||||
"private": true,
|
||||
"description": "Modular production dashboard for Waters Church — zero-dependency Node server + vanilla client",
|
||||
"description": "Modular production dashboard for Waters Church \u2014 zero-dependency Node server + vanilla client",
|
||||
"license": "UNLICENSED",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
* proddash.json in the data directory (this machine's overrides), or env vars:
|
||||
* PORT (default: value in proddash.json, else 24500)
|
||||
* PRODDASH_PASSCODE (overrides adminPasscode in proddash.json)
|
||||
* PRODDASH_FORGE (overrides `forge`: "github", or a Gitea/Forgejo base
|
||||
* URL such as http://10.3.8.90:3000)
|
||||
* PRODDASH_DATA_DIR (where module config + layouts are kept; see below)
|
||||
*/
|
||||
'use strict';
|
||||
@@ -764,15 +766,53 @@ function parseRepoSetting(value, fallbackBranch) {
|
||||
}
|
||||
|
||||
const REPO = parseRepoSetting(process.env.PRODDASH_REPO || shellConfig.repo, shellConfig.branch)
|
||||
|| parseRepoSetting('michaelmcgary2008/ProdDash', 'main');
|
||||
|| parseRepoSetting('michael/ProdDash', 'main');
|
||||
|
||||
/**
|
||||
* Which server the code comes from. GitHub unless `forge` names another —
|
||||
* give it a Gitea (or Forgejo) base URL, "http://10.3.8.90:3000", and updates
|
||||
* and module installs come from there instead. The two hosts differ only in
|
||||
* how an archive URL is spelled: the tarball itself is the same, pax header
|
||||
* and commit sha included, so everything downstream is untouched.
|
||||
*/
|
||||
function parseForge(value) {
|
||||
const s = String(value || '').trim().replace(/\/+$/, '');
|
||||
if (!s || s.toLowerCase() === 'github') {
|
||||
return { kind: 'github', base: 'https://github.com', archiveBase: 'https://codeload.github.com' };
|
||||
}
|
||||
if (!/^https?:\/\//i.test(s)) return null;
|
||||
// A GitHub URL still means codeload for archives.
|
||||
if (/^https?:\/\/(www\.)?github\.com$/i.test(s)) {
|
||||
return { kind: 'github', base: s, archiveBase: 'https://codeload.github.com' };
|
||||
}
|
||||
return { kind: 'gitea', base: s, archiveBase: s };
|
||||
}
|
||||
|
||||
const FORGE = parseForge(process.env.PRODDASH_FORGE || shellConfig.forge) || parseForge('github');
|
||||
/** Where branch archives come from; tests point this at a local mock. */
|
||||
const ARCHIVE_BASE = String(process.env.PRODDASH_ARCHIVE_BASE || 'https://codeload.github.com').replace(/\/+$/, '');
|
||||
const ARCHIVE_BASE = String(process.env.PRODDASH_ARCHIVE_BASE || FORGE.archiveBase).replace(/\/+$/, '');
|
||||
const REMOTE_TTL_MS = 10 * 60 * 1000;
|
||||
const REMOTE_CHECK_MS = 6 * 60 * 60 * 1000;
|
||||
const MAX_ARCHIVE_BYTES = 80 * 1024 * 1024;
|
||||
|
||||
function tarballUrl() {
|
||||
return `${ARCHIVE_BASE}/${REPO.owner}/${REPO.repo}/tar.gz/${encodeURIComponent(REPO.branch)}`;
|
||||
const branch = encodeURIComponent(REPO.branch);
|
||||
return FORGE.kind === 'gitea'
|
||||
? `${ARCHIVE_BASE}/${REPO.owner}/${REPO.repo}/archive/${branch}.tar.gz`
|
||||
: `${ARCHIVE_BASE}/${REPO.owner}/${REPO.repo}/tar.gz/${branch}`;
|
||||
}
|
||||
|
||||
/** The page a human should read to see what changed between two commits. */
|
||||
function forgeCompareUrl(from, to) {
|
||||
return `${FORGE.base}/${REPO.owner}/${REPO.repo}/compare/${from}...${to}`;
|
||||
}
|
||||
|
||||
/** The commit list for the branch we follow. */
|
||||
function forgeCommitsUrl() {
|
||||
const branch = encodeURIComponent(REPO.branch);
|
||||
return FORGE.kind === 'gitea'
|
||||
? `${FORGE.base}/${REPO.owner}/${REPO.repo}/commits/branch/${branch}`
|
||||
: `${FORGE.base}/${REPO.owner}/${REPO.repo}/commits/${branch}`;
|
||||
}
|
||||
|
||||
function httpError(status, message) {
|
||||
@@ -929,11 +969,11 @@ function shellStatus() {
|
||||
else if (local.branch !== REPO.branch) blocker = `this checkout is on branch "${local.branch}", not "${REPO.branch}"`;
|
||||
else if (local.dirty) blocker = 'this checkout has local changes — commit or discard them first';
|
||||
}
|
||||
const gh = `https://github.com/${REPO.owner}/${REPO.repo}`;
|
||||
return {
|
||||
version: SHELL_VERSION,
|
||||
repo: `${REPO.owner}/${REPO.repo}`,
|
||||
branch: REPO.branch,
|
||||
forge: FORGE.base,
|
||||
local: { isGit: local.isGit, sha: local.sha, branch: local.branch, dirty: local.dirty },
|
||||
remote: { version: remote.version, sha: remote.sha, checkedAt: remote.checkedAt, error: remote.error },
|
||||
updateAvailable: versionBehind || commitsBehind,
|
||||
@@ -941,8 +981,8 @@ function shellStatus() {
|
||||
method: local.isGit ? 'git' : 'archive',
|
||||
updateBlocker: blocker,
|
||||
changesUrl: local.sha && remote.sha
|
||||
? `${gh}/compare/${local.sha.slice(0, 12)}...${remote.sha.slice(0, 12)}`
|
||||
: `${gh}/commits/${REPO.branch}`,
|
||||
? forgeCompareUrl(local.sha.slice(0, 12), remote.sha.slice(0, 12))
|
||||
: forgeCommitsUrl(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1693,7 +1733,7 @@ server.on('listening', () => {
|
||||
? ' Modules : ' + ids.map((id) => id + (isEnabled(id) ? '' : ' (disabled)') + (manifests.get(id).incompatible ? ' (needs newer ProdDash)' : '')).join(', ')
|
||||
: ' Modules : none installed');
|
||||
console.log(' Settings : ' + DATA_DIR + (DATA_DIR === REPO_CONFIG_DIR ? ' (inside the app folder)' : ''));
|
||||
console.log(' Repo : ' + REPO.owner + '/' + REPO.repo + '@' + REPO.branch);
|
||||
console.log(' Repo : ' + REPO.owner + '/' + REPO.repo + '@' + REPO.branch + ' (' + FORGE.base + ')');
|
||||
});
|
||||
|
||||
// A restart hands the port over from the exiting ProdDash: wait for it.
|
||||
|
||||
Reference in New Issue
Block a user