Keep module config and layouts in a per-machine data directory
Connection settings, API keys, enabled flags and named layouts were written to config/ inside the checkout, so a re-clone or reset from main started the server with nothing configured. They now live in a data directory outside the app folder: Windows %APPDATA%\ProdDash macOS ~/Library/Application Support/ProdDash other $XDG_CONFIG_HOME/proddash, else ~/.config/proddash PRODDASH_DATA_DIR overrides the location (a relative path resolves against the app folder, so "config" restores the old spot). The first start after upgrading copies an existing config/modules.json and config/layouts/ into the data directory, so nothing has to be re-entered. If the directory cannot be created or written, the server warns and falls back to config/ rather than refusing to start. A proddash.json in the data directory overrides the checked-in defaults (port, adminPasscode). The startup banner prints the directory in use, /api/admin/state exposes it, and the admin page shows it under the Modules heading. Admin config and layout writes that fail on disk now report the path instead of a generic server error. README, MODULE-GUIDE and CLAUDE.md updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNYUv9EyrGSFmCnmH7WYKH
This commit is contained in:
@@ -3,7 +3,10 @@
|
|||||||
Modular production dashboard web app for Waters Church, developed in this repo.
|
Modular production dashboard web app for Waters Church, developed in this repo.
|
||||||
The app lives at the repo root and is built: zero-dependency Node server
|
The app lives at the repo root and is built: zero-dependency Node server
|
||||||
(`server.js`, Node 18+), dashboard shell client in `public/`, admin page at
|
(`server.js`, Node 18+), dashboard shell client in `public/`, admin page at
|
||||||
`/admin`, modules in `modules/`. [SPEC.md](SPEC.md) was the authority for the
|
`/admin`, modules in `modules/`. Runtime state (module config, named layouts)
|
||||||
|
is written to a per-machine data directory outside the checkout
|
||||||
|
(`PRODDASH_DATA_DIR` overrides; see README "Where settings are kept") — never
|
||||||
|
add code that writes runtime state into the repo folder. [SPEC.md](SPEC.md) was the authority for the
|
||||||
initial build; [README.md](README.md) has run/configure/use instructions.
|
initial build; [README.md](README.md) has run/configure/use instructions.
|
||||||
|
|
||||||
## Building a new module — read docs/MODULE-GUIDE.md first
|
## Building a new module — read docs/MODULE-GUIDE.md first
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ touching the shell (see [docs/MODULE-GUIDE.md](docs/MODULE-GUIDE.md)).
|
|||||||
that's where module servers proxy ProdCom, poll ProPresenter, and stream
|
that's where module servers proxy ProdCom, poll ProPresenter, and stream
|
||||||
live updates over SSE, so browsers never talk to production gear
|
live updates over SSE, so browsers never talk to production gear
|
||||||
directly (no CORS, and API keys never leave the server).
|
directly (no CORS, and API keys never leave the server).
|
||||||
4. Stores server-wide module config and named layouts under `config/`.
|
4. Stores server-wide module config and named layouts in a per-machine data
|
||||||
|
directory outside the app folder, so updating ProdDash never loses them.
|
||||||
|
|
||||||
Each browser arranges its own tiles (add, drag, resize, remove, per-tile
|
Each browser arranges its own tiles (add, drag, resize, remove, per-tile
|
||||||
settings); the layout autosaves to that browser's `localStorage`. Named
|
settings); the layout autosaves to that browser's `localStorage`. Named
|
||||||
@@ -47,16 +48,43 @@ enable/disable) live in the **admin page** — open **Admin** from the
|
|||||||
dashboard's top bar, or go to `/admin`. Saving re-initialises the module
|
dashboard's top bar, or go to `/admin`. Saving re-initialises the module
|
||||||
immediately; open tiles reconnect on their own.
|
immediately; open tiles reconnect on their own.
|
||||||
|
|
||||||
Shell settings live in [config/proddash.json](config/proddash.json):
|
### Where settings are kept
|
||||||
|
|
||||||
|
Everything you enter in the admin page (module connection settings, API keys,
|
||||||
|
enabled/disabled flags) and every named layout is written to a **data
|
||||||
|
directory on the machine running the server** — not into the app folder:
|
||||||
|
|
||||||
|
| OS | Data directory |
|
||||||
|
| --- | --- |
|
||||||
|
| Windows | `%APPDATA%\ProdDash` (e.g. `C:\Users\<you>\AppData\Roaming\ProdDash`) |
|
||||||
|
| macOS | `~/Library/Application Support/ProdDash` |
|
||||||
|
| Linux | `$XDG_CONFIG_HOME/proddash`, else `~/.config/proddash` |
|
||||||
|
|
||||||
|
Because it lives outside the checkout, you can `git pull`, re-clone, or reset
|
||||||
|
the app folder from `main` and start the new version with everything still
|
||||||
|
configured. The admin page shows the exact path in use, and the server prints
|
||||||
|
it at startup (`Settings : …`).
|
||||||
|
|
||||||
|
The directory holds `modules.json` (module config) and `layouts/` (named
|
||||||
|
layouts), both written by the app itself. It may also hold a `proddash.json`
|
||||||
|
with this machine's shell settings — the same keys as the checked-in
|
||||||
|
[config/proddash.json](config/proddash.json), which supplies the defaults:
|
||||||
|
|
||||||
| Key | Meaning |
|
| Key | Meaning |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `port` | Port ProdDash serves on (default `24500`) |
|
| `port` | Port ProdDash serves on (default `24500`) |
|
||||||
| `adminPasscode` | If set, the admin page asks for this shared passcode. Leave `""` for none — it's a plain-HTTP LAN tool. |
|
| `adminPasscode` | If set, the admin page asks for this shared passcode. Leave `""` for none — it's a plain-HTTP LAN tool. |
|
||||||
|
|
||||||
Environment variables `PORT` and `PRODDASH_PASSCODE` override the file.
|
Environment variables override both files: `PORT`, `PRODDASH_PASSCODE`, and
|
||||||
`config/modules.json` (module config) and `config/layouts/` (named layouts)
|
`PRODDASH_DATA_DIR` to put the data directory somewhere else (a relative path
|
||||||
are written by the app itself.
|
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.
|
||||||
|
|
||||||
|
**Upgrading from an earlier version:** the first start copies an existing
|
||||||
|
`config/modules.json` and `config/layouts/` from the app folder into the data
|
||||||
|
directory, so nothing has to be re-entered. The old files are left in place and
|
||||||
|
ignored from then on.
|
||||||
|
|
||||||
## Using the dashboard
|
## Using the dashboard
|
||||||
|
|
||||||
@@ -130,7 +158,8 @@ ProdDash/
|
|||||||
│ ├── clock/
|
│ ├── clock/
|
||||||
│ ├── prodcom-transcript/
|
│ ├── prodcom-transcript/
|
||||||
│ └── propresenter-now-next/ (carries its own copy of propresenter-core)
|
│ └── propresenter-now-next/ (carries its own copy of propresenter-core)
|
||||||
├── config/ proddash.json + state written by the app
|
├── config/ proddash.json defaults (runtime state lives in the
|
||||||
|
│ per-machine data directory — see "Configure")
|
||||||
├── docs/MODULE-GUIDE.md how to build and install a module
|
├── docs/MODULE-GUIDE.md how to build and install a module
|
||||||
├── tools/prodcom-mock.js mock ProdCom API for development
|
├── tools/prodcom-mock.js mock ProdCom API for development
|
||||||
├── prodcom-listener/ reference fork — read-only, not served
|
├── prodcom-listener/ reference fork — read-only, not served
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ Both schemas map field names to specs:
|
|||||||
```
|
```
|
||||||
|
|
||||||
`password` fields are admin-config only in practice: their values are stored
|
`password` fields are admin-config only in practice: their values are stored
|
||||||
in `config/modules.json`, handed to your *server* entry, and **never sent to
|
in the server's `modules.json` (in the per-machine data directory, see the
|
||||||
browsers** — not in `/api/modules`, and never echoed back into the admin
|
README), handed to your *server* entry, and **never sent to browsers** — not in `/api/modules`, and never echoed back into the admin
|
||||||
form. Anything upstream that needs the secret must go through your server
|
form. Anything upstream that needs the secret must go through your server
|
||||||
routes.
|
routes.
|
||||||
|
|
||||||
@@ -105,8 +105,9 @@ web address where protocol or path matter (a page to embed, a webhook).
|
|||||||
Endpoints belong in admin config, not per-tile settings — connections are
|
Endpoints belong in admin config, not per-tile settings — connections are
|
||||||
server-wide by design.
|
server-wide by design.
|
||||||
|
|
||||||
Admin config lives in `config/modules.json` (written by the admin page —
|
Admin config lives in `modules.json` inside the server's data directory
|
||||||
your module never touches that file). Per-tile settings live inside each
|
(written by the admin page — your module never touches that file, and it
|
||||||
|
survives updates of the app folder). Per-tile settings live inside each
|
||||||
browser's layout.
|
browser's layout.
|
||||||
|
|
||||||
## Client entry (`client.js`)
|
## Client entry (`client.js`)
|
||||||
|
|||||||
@@ -35,6 +35,18 @@ body { overflow: auto; }
|
|||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-storage { margin-top: -6px; font-size: 12px; }
|
||||||
|
.admin-storage code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-card {
|
.admin-card {
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
|||||||
@@ -382,6 +382,11 @@ async function loadState() {
|
|||||||
}
|
}
|
||||||
loginCard.hidden = true;
|
loginCard.hidden = true;
|
||||||
modulesSection.hidden = false;
|
modulesSection.hidden = false;
|
||||||
|
const storageNote = document.getElementById('storage-note');
|
||||||
|
if (state.dataDir) {
|
||||||
|
document.getElementById('storage-path').textContent = state.dataDir;
|
||||||
|
storageNote.hidden = false;
|
||||||
|
}
|
||||||
modulesList.innerHTML = '';
|
modulesList.innerHTML = '';
|
||||||
liveEls.clear();
|
liveEls.clear();
|
||||||
for (const mod of state.modules || []) {
|
for (const mod of state.modules || []) {
|
||||||
|
|||||||
@@ -32,6 +32,8 @@
|
|||||||
<p class="admin-copy">Server-wide settings, shared by every dashboard. Saving re-initialises the
|
<p class="admin-copy">Server-wide settings, shared by every dashboard. Saving re-initialises the
|
||||||
module immediately — open tiles reconnect on their own. Per-tile options (filters, text size)
|
module immediately — open tiles reconnect on their own. Per-tile options (filters, text size)
|
||||||
live in each tile's gear menu instead.</p>
|
live in each tile's gear menu instead.</p>
|
||||||
|
<p class="admin-copy admin-storage" id="storage-note" hidden>Saved on the server at
|
||||||
|
<code id="storage-path"></code> — outside the app folder, so updating ProdDash keeps them.</p>
|
||||||
<div id="modules-list"></div>
|
<div id="modules-list"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,16 @@
|
|||||||
*
|
*
|
||||||
* Shell server: serves the dashboard client from public/, discovers modules
|
* Shell server: serves the dashboard client from public/, discovers modules
|
||||||
* in modules/, mounts each enabled module's server routes under
|
* in modules/, mounts each enabled module's server routes under
|
||||||
* /api/modules/<id>/, and stores server-side config + named layouts under
|
* /api/modules/<id>/, and stores server-side module config + named layouts
|
||||||
* config/.
|
* in a per-machine data directory (see DATA_DIR below) so that updating the
|
||||||
|
* checkout never loses them.
|
||||||
*
|
*
|
||||||
* Zero dependencies — Node built-ins only (Node 18+ for global fetch).
|
* Zero dependencies — Node built-ins only (Node 18+ for global fetch).
|
||||||
* Configure via config/proddash.json or env vars:
|
* Configure via config/proddash.json (defaults, in the repo), an optional
|
||||||
|
* proddash.json in the data directory (this machine's overrides), or env vars:
|
||||||
* PORT (default: value in proddash.json, else 24500)
|
* PORT (default: value in proddash.json, else 24500)
|
||||||
* PRODDASH_PASSCODE (overrides adminPasscode in proddash.json)
|
* PRODDASH_PASSCODE (overrides adminPasscode in proddash.json)
|
||||||
|
* PRODDASH_DATA_DIR (where module config + layouts are kept; see below)
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
@@ -22,9 +25,54 @@ const crypto = require('crypto');
|
|||||||
|
|
||||||
const ROOT = __dirname;
|
const ROOT = __dirname;
|
||||||
const PUBLIC_DIR = path.join(ROOT, 'public');
|
const PUBLIC_DIR = path.join(ROOT, 'public');
|
||||||
const CONFIG_DIR = path.join(ROOT, 'config');
|
|
||||||
const MODULES_DIR = path.join(ROOT, 'modules');
|
const MODULES_DIR = path.join(ROOT, 'modules');
|
||||||
const MODULES_CONFIG_PATH = path.join(CONFIG_DIR, 'modules.json');
|
/** Checked-in defaults (proddash.json) — and where runtime state used to live. */
|
||||||
|
const REPO_CONFIG_DIR = path.join(ROOT, 'config');
|
||||||
|
|
||||||
|
/* ── data directory ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Everything the app writes — module config (connection settings, API keys,
|
||||||
|
* enabled flags) and named layouts — lives in a per-machine data directory
|
||||||
|
* OUTSIDE the checkout. Pulling, re-cloning or resetting the app folder from
|
||||||
|
* main therefore never touches it, and nothing has to be re-entered in /admin.
|
||||||
|
*
|
||||||
|
* PRODDASH_DATA_DIR overrides the location. A relative path resolves
|
||||||
|
* against the app folder ("config" = the old in-repo spot).
|
||||||
|
* default Windows %APPDATA%\ProdDash
|
||||||
|
* macOS ~/Library/Application Support/ProdDash
|
||||||
|
* other $XDG_CONFIG_HOME/proddash, else ~/.config/proddash
|
||||||
|
*
|
||||||
|
* If the directory cannot be created or written, the server says so and
|
||||||
|
* falls back to the in-repo config/ folder rather than refusing to start.
|
||||||
|
*/
|
||||||
|
function defaultDataDir() {
|
||||||
|
const home = os.homedir();
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
return path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'ProdDash');
|
||||||
|
}
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
return path.join(home, 'Library', 'Application Support', 'ProdDash');
|
||||||
|
}
|
||||||
|
return path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'proddash');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDataDir() {
|
||||||
|
const fromEnv = String(process.env.PRODDASH_DATA_DIR || '').trim();
|
||||||
|
const dir = fromEnv ? path.resolve(ROOT, fromEnv) : defaultDataDir();
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
fs.accessSync(dir, fs.constants.W_OK);
|
||||||
|
return dir;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[proddash] cannot use data directory ${dir} (${err.message}) — falling back to ${REPO_CONFIG_DIR}`);
|
||||||
|
return REPO_CONFIG_DIR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DATA_DIR = resolveDataDir();
|
||||||
|
const MODULES_CONFIG_PATH = path.join(DATA_DIR, 'modules.json');
|
||||||
|
const LAYOUTS_DIR = path.join(DATA_DIR, 'layouts');
|
||||||
|
|
||||||
/* ── config files ───────────────────────────────────────────────────── */
|
/* ── config files ───────────────────────────────────────────────────── */
|
||||||
|
|
||||||
@@ -43,7 +91,52 @@ function writeJson(file, value) {
|
|||||||
fs.renameSync(tmp, file); // atomic-ish: never leave a half-written config
|
fs.renameSync(tmp, file); // atomic-ish: never leave a half-written config
|
||||||
}
|
}
|
||||||
|
|
||||||
const shellConfig = readJson(path.join(CONFIG_DIR, 'proddash.json'), {});
|
/**
|
||||||
|
* One-time move-in. Earlier versions kept modules.json and layouts/ inside the
|
||||||
|
* repo's config/ folder; if the data directory has none yet and those files
|
||||||
|
* exist, copy them across (the originals are left alone).
|
||||||
|
*/
|
||||||
|
function migrateLegacyState() {
|
||||||
|
if (DATA_DIR === REPO_CONFIG_DIR) return;
|
||||||
|
const moved = [];
|
||||||
|
const oldModules = path.join(REPO_CONFIG_DIR, 'modules.json');
|
||||||
|
if (!fs.existsSync(MODULES_CONFIG_PATH) && fs.existsSync(oldModules)) {
|
||||||
|
try {
|
||||||
|
fs.copyFileSync(oldModules, MODULES_CONFIG_PATH);
|
||||||
|
moved.push('modules.json');
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[proddash] could not copy ${oldModules} into the data directory: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const oldLayouts = path.join(REPO_CONFIG_DIR, 'layouts');
|
||||||
|
let oldFiles = [];
|
||||||
|
try {
|
||||||
|
oldFiles = fs.readdirSync(oldLayouts).filter((f) => f.endsWith('.json'));
|
||||||
|
} catch { /* none to move */ }
|
||||||
|
let haveLayouts = false;
|
||||||
|
try {
|
||||||
|
haveLayouts = fs.readdirSync(LAYOUTS_DIR).some((f) => f.endsWith('.json'));
|
||||||
|
} catch { /* no layouts dir yet */ }
|
||||||
|
if (oldFiles.length && !haveLayouts) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(LAYOUTS_DIR, { recursive: true });
|
||||||
|
for (const f of oldFiles) fs.copyFileSync(path.join(oldLayouts, f), path.join(LAYOUTS_DIR, f));
|
||||||
|
moved.push(`${oldFiles.length} layout${oldFiles.length === 1 ? '' : 's'}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[proddash] could not copy layouts into the data directory: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (moved.length) {
|
||||||
|
console.log(`[proddash] moved ${moved.join(' and ')} from ${REPO_CONFIG_DIR} into ${DATA_DIR}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
migrateLegacyState();
|
||||||
|
|
||||||
|
/** Shell settings: checked-in defaults, then this machine's overrides on top. */
|
||||||
|
const shellConfig = {
|
||||||
|
...readJson(path.join(REPO_CONFIG_DIR, 'proddash.json'), {}),
|
||||||
|
...readJson(path.join(DATA_DIR, 'proddash.json'), {}),
|
||||||
|
};
|
||||||
const PORT = Number(process.env.PORT || shellConfig.port || 24500);
|
const PORT = Number(process.env.PORT || shellConfig.port || 24500);
|
||||||
|
|
||||||
/** Per-module server-wide state: { "<id>": { enabled: bool, config: {…} } } */
|
/** Per-module server-wide state: { "<id>": { enabled: bool, config: {…} } } */
|
||||||
@@ -453,6 +546,7 @@ async function handleAdminApi(req, res, urlPath) {
|
|||||||
return sendJson(res, 200, {
|
return sendJson(res, 200, {
|
||||||
authRequired: Boolean(ADMIN_PASSCODE),
|
authRequired: Boolean(ADMIN_PASSCODE),
|
||||||
authed: true,
|
authed: true,
|
||||||
|
dataDir: DATA_DIR,
|
||||||
modules: [...manifests.keys()].sort().map(adminModuleView),
|
modules: [...manifests.keys()].sort().map(adminModuleView),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -468,15 +562,19 @@ async function handleAdminApi(req, res, urlPath) {
|
|||||||
} catch {
|
} catch {
|
||||||
return sendJson(res, 400, { error: 'Malformed request.' });
|
return sendJson(res, 400, { error: 'Malformed request.' });
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
if (m[2] === 'config') {
|
if (m[2] === 'config') {
|
||||||
applyConfigPatch(id, body.config);
|
applyConfigPatch(id, body.config);
|
||||||
remountModule(id); // config changes take effect without a manual restart
|
|
||||||
} else {
|
} else {
|
||||||
const enabled = Boolean(body.enabled);
|
modulesConfig[id] = { ...moduleState(id), enabled: Boolean(body.enabled) };
|
||||||
modulesConfig[id] = { ...moduleState(id), enabled };
|
|
||||||
writeJson(MODULES_CONFIG_PATH, modulesConfig);
|
writeJson(MODULES_CONFIG_PATH, modulesConfig);
|
||||||
remountModule(id); // mounts when enabled, unmounts (only) otherwise
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return sendJson(res, 500, { error: `Could not save ${MODULES_CONFIG_PATH}: ${err?.message || err}` });
|
||||||
|
}
|
||||||
|
// Config changes take effect without a manual restart; a toggle mounts when
|
||||||
|
// enabled and unmounts (only) otherwise.
|
||||||
|
remountModule(id);
|
||||||
broadcastShellEvent('modules-changed', { id });
|
broadcastShellEvent('modules-changed', { id });
|
||||||
return sendJson(res, 200, { ok: true, module: adminModuleView(id) });
|
return sendJson(res, 200, { ok: true, module: adminModuleView(id) });
|
||||||
}
|
}
|
||||||
@@ -484,9 +582,7 @@ async function handleAdminApi(req, res, urlPath) {
|
|||||||
sendJson(res, 404, { error: 'Unknown admin endpoint.' });
|
sendJson(res, 404, { error: 'Unknown admin endpoint.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── named layouts (JSON files under config/layouts/) ───────────────── */
|
/* ── named layouts (JSON files under <data dir>/layouts/) ───────────── */
|
||||||
|
|
||||||
const LAYOUTS_DIR = path.join(CONFIG_DIR, 'layouts');
|
|
||||||
|
|
||||||
/** Names double as file names — keep them boring on purpose. */
|
/** Names double as file names — keep them boring on purpose. */
|
||||||
function layoutNameOk(name) {
|
function layoutNameOk(name) {
|
||||||
@@ -564,7 +660,11 @@ async function handleLayoutsApi(req, res, urlPath) {
|
|||||||
}
|
}
|
||||||
const layout = sanitizeLayout(body.layout);
|
const layout = sanitizeLayout(body.layout);
|
||||||
if (!layout.tiles.length) return sendJson(res, 400, { error: 'Refusing to save an empty layout.' });
|
if (!layout.tiles.length) return sendJson(res, 400, { error: 'Refusing to save an empty layout.' });
|
||||||
|
try {
|
||||||
writeJson(layoutPath(name), layout);
|
writeJson(layoutPath(name), layout);
|
||||||
|
} catch (err) {
|
||||||
|
return sendJson(res, 500, { error: `Could not save ${layoutPath(name)}: ${err?.message || err}` });
|
||||||
|
}
|
||||||
return sendJson(res, 200, { ok: true, name });
|
return sendJson(res, 200, { ok: true, name });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -824,6 +924,7 @@ server.listen(PORT, '0.0.0.0', () => {
|
|||||||
console.log(ids.length
|
console.log(ids.length
|
||||||
? ' Modules : ' + ids.map((id) => id + (isEnabled(id) ? '' : ' (disabled)')).join(', ')
|
? ' Modules : ' + ids.map((id) => id + (isEnabled(id) ? '' : ' (disabled)')).join(', ')
|
||||||
: ' Modules : none installed');
|
: ' Modules : none installed');
|
||||||
|
console.log(' Settings : ' + DATA_DIR + (DATA_DIR === REPO_CONFIG_DIR ? ' (inside the app folder)' : ''));
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const signal of ['SIGINT', 'SIGTERM']) {
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
||||||
|
|||||||
Reference in New Issue
Block a user