Add AUTH_STACK so the Auth tab follows the stack in force

Authentik is the likely destination and the page had Authelia and lldap wired in at every
level, so the seam goes in now: the panels and every endpoint action route off one conf value,
and a stack that cannot be driven yet says so rather than drawing controls with nothing behind.
This commit is contained in:
Gmer4Lfe
2026-08-15 15:27:50 -04:00
parent 2697b46616
commit 8157673291
6 changed files with 219 additions and 25 deletions
+18 -1
View File
@@ -1439,6 +1439,23 @@
# Defined per host in host*.conf — Emby container names and keys differ per server:
# HOST1_TRANSCODE_SERVERS / HOST2_TRANSCODE_SERVERS
# ==============================================================================================
# ── AUTH STACK ────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
# ━━━ Auth Stack ━━━
# Which identity stack this mesh runs behind its protected hostnames. The Auth tab reads this to
# decide which panels to draw and which endpoints to call — it is a routing switch, not a
# migration. Changing it does not move users, groups or rules between stacks; stand the new one
# up first, then point this at it.
#
# Mesh-wide rather than per host: auth is the one service the partnership treats as shared, owned
# by the mesh owner and consumed by everyone else, so two nodes disagreeing about which stack is
# in force would mean two different answers to "who is this person".
# "authelia_lldap" — Authelia for access rules, lldap for users and groups. Fully implemented.
# "authentik" — single stack for both. NOT IMPLEMENTED YET; the tab says so instead of drawing panels that cannot work.
AUTH_STACK="authelia_lldap"
# ==============================================================================================
# ── MONITORS ──────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
@@ -1706,12 +1723,12 @@
# earned it — narration for months before anything is allowed near a decision.
# ━━━ AI Master Switch ━━━
# Fail-closed: anything other than the literal "true" means off.
# Which node runs the model, the retrieval index and the bug store. Every other node borrows it
# over the mesh, so a box without a GPU still gets the assistant — it just does not get the AI tab.
# Defaults to host1 when unset or malformed: whoever builds the mesh is host1.
AI_OWNER_HOST="host1"
# Fail-closed: anything other than the literal "true" means off.
AI_ENABLED=false
AI_CONNECT_TIMEOUT=5 # seconds — probe when resolving which node has Ollama
AI_REQUEST_TIMEOUT=240 # seconds — must clear a cold model load
+37
View File
@@ -93,8 +93,44 @@
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/auth.php';
// Which panel each action belongs to. AUTH_STACK decides which panels the tab draws, and this is
// the same decision applied to the endpoint — a tab left open from before a switch would otherwise
// keep writing to the stack that is no longer in force, which on this page means editing the
// directory or the rules of a system nobody is authenticating against any more.
//
// Proxies and certs are Nginx Proxy Manager's, not the identity stack's, so they are listed under
// panels every stack carries rather than gated to one.
const VV_AUTH_ACTION_PANEL = [
// GET
'npm_proxies' => 'proxies', 'npm_certs' => 'proxies',
'lldap_users' => 'users', 'lldap_groups' => 'users', 'lldap_avatar' => 'users',
'authelia_rules' => 'acl',
// POST
'npm_create' => 'proxies', 'npm_update' => 'proxies',
'npm_delete' => 'proxies', 'npm_toggle' => 'proxies',
'lldap_create_user' => 'users', 'lldap_update_user' => 'users', 'lldap_delete_user' => 'users',
'lldap_set_password' => 'users', 'lldap_set_avatar' => 'users', 'lldap_remove_avatar' => 'users',
'lldap_create_group' => 'users', 'lldap_delete_group' => 'users', 'lldap_rename_group' => 'users',
'lldap_add_to_group' => 'users', 'lldap_remove_from_group' => 'users',
'authelia_save' => 'acl',
];
function vv_auth_action_allowed(string $action): bool {
$panel = VV_AUTH_ACTION_PANEL[$action] ?? null;
// Unmapped actions are left to the existing "Unknown action" answer rather than being refused
// here, so a new action is never silently blocked by a table someone forgot to extend.
return $panel === null || vv_auth_panel_on($panel);
}
function vv_auth_action_refusal(string $action): array {
$d = vv_auth_stack_def();
return ['ok' => false, 'error' => 'AUTH_STACK is "' . vv_auth_stack() . '" (' . $d['label']
. '), which does not serve this request. Reload the Auth tab.'];
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$action = $_GET['action'] ?? '';
if (!vv_auth_action_allowed($action)) { echo json_encode(vv_auth_action_refusal($action)); exit; }
// The one route here that does not answer in JSON — it streams the stored JPEG so the page can
// point an <img> at it, rather than carrying 470 KB of base64 through the user list on every
@@ -133,6 +169,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
}
$action = trim($_POST['action'] ?? '');
if (!vv_auth_action_allowed($action)) { echo json_encode(vv_auth_action_refusal($action)); exit; }
$result = match ($action) {
// NPM
+51
View File
@@ -98,6 +98,57 @@ function vv_auth_conf(): array {
];
}
// ── Which stack ───────────────────────────────────────────────────────────────
// The stacks this page knows how to drive, and how far it can drive each one. Declared rather
// than inferred so the tab can offer a stack it cannot yet operate and say exactly that, instead
// of drawing panels that call endpoints with nothing behind them — which is how a switch ends up
// looking like a feature while reading nothing.
const VV_AUTH_STACKS = [
'authelia_lldap' => [
'label' => 'Authelia + lldap',
'ready' => true,
'panels' => ['proxies', 'users', 'acl', 'certs'],
'summary' => 'Authelia holds the access rules, lldap holds users and groups, '
. 'Nginx Proxy Manager holds the hostnames.',
],
'authentik' => [
'label' => 'Authentik',
'ready' => false,
// Proxies and certs are NPM's, not the identity stack's, so they keep working whichever
// stack is selected. Users and access control are the two this page cannot draw yet.
'panels' => ['proxies', 'certs'],
'summary' => 'One stack for identity and access. Varaverk can still manage the proxy '
. 'hosts and certificates, but not Authentik users, groups or policies yet.',
'needs' => 'An API token and base URL in host*.conf, then user, group and policy '
. 'calls against Authentik\'s REST API to sit behind the same page.',
],
];
// Falls back rather than failing: an unrecognised value means someone typed a stack name into the
// conf, and answering with a blank tab helps nobody. The working stack is the safe answer, and
// vv_auth_stack_valid() is what the page uses to say the value was not understood.
function vv_auth_stack(): string {
$v = trim(vv_conf_vars()['AUTH_STACK'] ?? '');
return isset(VV_AUTH_STACKS[$v]) ? $v : 'authelia_lldap';
}
function vv_auth_stack_valid(): bool {
$v = trim(vv_conf_vars()['AUTH_STACK'] ?? '');
return $v === '' || isset(VV_AUTH_STACKS[$v]);
}
function vv_auth_stack_def(): array {
return VV_AUTH_STACKS[vv_auth_stack()];
}
// One question, asked the same way by the page and by the endpoint. The page uses it to decide
// what to draw; api/auth.php uses it to refuse an action belonging to a stack that is not the one
// in force, so a stale tab left open across a switch cannot write to the wrong directory.
function vv_auth_panel_on(string $panel): bool {
return in_array($panel, vv_auth_stack_def()['panels'], true);
}
// ── Credential state ──────────────────────────────────────────────────────────
// Three different failures arrive at a token fetch as the same empty string: the credential was
+1 -1
View File
@@ -128,7 +128,7 @@ const VV_UI_SECTION_SURFACES = [
// is what its Certs panel reports. The credentials in particular belong on the page that fails
// without them: a blank NPM_USER surfaces there as a refused login, and the fix is two panels
// away rather than in a hundred-and-twenty-field catch-all.
['match' => 'NginxProxyManager|lldap|Authelia|Certificate Monitor', 'tab' => 'Auth',
['match' => 'Auth Stack|NginxProxyManager|lldap|Authelia|Certificate Monitor', 'tab' => 'Auth',
'route' => 'Auth tab → Auth settings'],
// Everything that is not structurally excluded. The catch-all exists so no ordinary setting
+86 -21
View File
@@ -37,6 +37,12 @@
// LLDAP user and group management
// Authelia access-control rules and default policy
//
// STACK SWITCHING
// AUTH_STACK in master.conf decides which panels exist. Proxies and Certs belong to Nginx
// Proxy Manager and are drawn for every stack; Users and Access Control are the identity
// stack's and are drawn only for one that Varaverk can drive. api/auth.php applies the same
// rule to every action, so a tab left open across a switch cannot write to the old stack.
//
// DEPENDS ON
// include/auth.php required directly for initial render
// api/auth.php mutations
@@ -60,6 +66,17 @@ require_once dirname(__DIR__) . '/include/ai_chat.php';
.vv-au-btn.green{ border-color:#1a3a1a;background:#0d1f0d;color:#4caf50; }
.vv-au-btn:disabled { opacity:.4;cursor:default; }
/* ── Stack indicator ─────────────────────────────────────────────────────── */
/* Which stack the tab is driving. Always visible, because every panel below it means something
different depending on this, and it is a conf value nothing else on the page reveals. */
.vv-au-stackchip{ font-size:10px;padding:2px 8px;border-radius:3px;background:#12181f;
border:1px solid #1e2a38;color:#5c9fd4;white-space:nowrap; }
.vv-au-stacknote{ font-size:11px;color:#997;background:#1a1400;border:1px solid #3a2d00;
border-radius:4px;padding:8px 12px;margin-bottom:10px;line-height:1.55; }
.vv-au-stacknote b { color:#ffb74d;display:block;margin-bottom:2px; }
.vv-au-stacknote.bad { background:#1a0d0d;border-color:#3a1a1a;color:#a77; }
.vv-au-stacknote.bad b { color:#ef5350; }
/* ── Panels ──────────────────────────────────────────────────────────────── */
.vv-au-panel { display:none; }
.vv-au-panel.active { display:block; }
@@ -231,19 +248,45 @@ require_once dirname(__DIR__) . '/include/ai_chat.php';
<?php
require_once __DIR__ . '/../include/auth.php';
$isOwner = vv_is_owner();
// AUTH_STACK decides which panels exist at all. Read once here and used for both the tabs and the
// panels, so a tab can never be drawn for a panel the stack does not carry.
$stackDef= vv_auth_stack_def();
$panels = $stackDef['panels'];
$first = $panels[0] ?? 'proxies';
$tabs = ['proxies' => 'Proxies', 'users' => 'Users &amp; Groups',
'acl' => 'Access Control', 'certs' => 'Certs'];
?>
<div class="vv-au-toolbar">
<span class="vv-au-title">Auth Stack</span>
<button class="vv-au-tab active" data-tab="proxies">Proxies</button>
<button class="vv-au-tab" data-tab="users">Users &amp; Groups</button>
<button class="vv-au-tab" data-tab="acl">Access Control</button>
<button class="vv-au-tab" data-tab="certs">Certs</button>
<?php foreach ($tabs as $id => $label): if (!in_array($id, $panels, true)) continue; ?>
<button class="vv-au-tab<?= $id === $first ? ' active' : '' ?>" data-tab="<?= $id ?>"><?= $label ?></button>
<?php endforeach; ?>
<span class="vv-au-stackchip" title="AUTH_STACK in master.conf — Settings below"><?= htmlspecialchars($stackDef['label']) ?></span>
<button class="vv-au-btn" id="vv-au-refresh" title="Refresh current tab">&#8635; Refresh</button>
</div>
<?php if (!vv_auth_stack_valid()): ?>
<!-- A value that is not one of the known stacks. Named rather than silently corrected, because
the tab is now showing a different stack than the conf asks for and that is worth knowing. -->
<div class="vv-au-stacknote bad">
<b>AUTH_STACK is set to "<?= htmlspecialchars(trim(vv_conf_vars()['AUTH_STACK'] ?? '')) ?>", which is not a stack this page knows.</b>
Showing <?= htmlspecialchars($stackDef['label']) ?> instead. Fix it in Auth settings, below.
</div>
<?php elseif (!$stackDef['ready']): ?>
<!-- Selected but not implemented. The panels that do work are still drawn — proxy hosts and
certificates belong to Nginx Proxy Manager, not to the identity stack — and the two that
cannot are absent with the reason stated, rather than present and broken. -->
<div class="vv-au-stacknote">
<b><?= htmlspecialchars($stackDef['label']) ?> is selected, and Varaverk cannot manage it yet.</b>
<?= htmlspecialchars($stackDef['summary']) ?>
<span style="color:#4a4a4a">Needed: <?= htmlspecialchars($stackDef['needs'] ?? '') ?></span>
</div>
<?php endif; ?>
<!-- ── Proxies ─────────────────────────────────────────────────────────────── -->
<div class="vv-au-panel active" id="vv-au-panel-proxies">
<?php if (in_array("proxies",$panels,true)): ?>
<div class="vv-au-panel<?= $first==="proxies" ? " active" : "" ?>" id="vv-au-panel-proxies">
<div class="vv-au-sec-bar">
<span class="vv-au-sec-title" id="vv-au-proxy-count"></span>
<button class="vv-au-btn prim" id="vv-au-proxy-add">+ Add Proxy</button>
@@ -259,9 +302,11 @@ $isOwner = vv_is_owner();
<div class="vv-au-empty" id="vv-au-proxy-empty" style="display:none">No proxy hosts configured.</div>
</div>
</div>
<?php endif; ?>
<!-- ── Users & Groups ─────────────────────────────────────────────────────── -->
<div class="vv-au-panel" id="vv-au-panel-users">
<?php if (in_array("users",$panels,true)): ?>
<div class="vv-au-panel<?= $first==="users" ? " active" : "" ?>" id="vv-au-panel-users">
<div class="vv-au-ug-grid">
<div class="vv-au-card">
@@ -286,9 +331,11 @@ $isOwner = vv_is_owner();
</div>
</div>
<?php endif; ?>
<!-- ── Access Control ─────────────────────────────────────────────────────── -->
<div class="vv-au-panel" id="vv-au-panel-acl">
<?php if (in_array("acl",$panels,true)): ?>
<div class="vv-au-panel<?= $first==="acl" ? " active" : "" ?>" id="vv-au-panel-acl">
<div class="vv-au-ac-defpol" id="vv-au-ac-defpol-bar">
<span>Default policy:</span>
<select id="vv-au-ac-defpol" <?= $isOwner ? '' : 'disabled' ?>>
@@ -315,9 +362,11 @@ $isOwner = vv_is_owner();
<div class="vv-au-acl-grid" id="vv-au-acl-body" style="display:none"></div>
<div class="vv-au-card"><div class="vv-au-empty" id="vv-au-acl-empty" style="display:none">No rules configured.</div></div>
</div>
<?php endif; ?>
<!-- ── Certs ───────────────────────────────────────────────────────────────── -->
<div class="vv-au-panel" id="vv-au-panel-certs">
<?php if (in_array("certs",$panels,true)): ?>
<div class="vv-au-panel<?= $first==="certs" ? " active" : "" ?>" id="vv-au-panel-certs">
<div class="vv-au-sec-bar">
<span class="vv-au-sec-title" id="vv-au-cert-ts"></span>
<button class="vv-au-btn prim" id="vv-au-cert-run">Refresh</button>
@@ -327,6 +376,7 @@ $isOwner = vv_is_owner();
</div>
<div id="vv-au-cert-cfg" style="margin-top:10px;font-size:10px;color:#3a3a3a;"></div>
</div>
<?php endif; ?>
<!-- ── Modal overlay ──────────────────────────────────────────────────────── -->
<div class="vv-au-overlay" id="vv-au-overlay">
@@ -368,7 +418,7 @@ $isOwner = vv_is_owner();
//
// Both passwords render masked and are logged by name only — vv_conf_key_is_secret() matches
// PASS, so the same key cannot be redacted in the audit log and legible in the form.
vv_conf_ui_card('vv-cf-auth', 'NginxProxyManager|lldap|Authelia|Certificate Monitor', 'Auth settings');
vv_conf_ui_card('vv-cf-auth', 'Auth Stack|NginxProxyManager|lldap|Authelia|Certificate Monitor', 'Auth settings');
?>
<script>
@@ -386,7 +436,10 @@ let _rules = [], _defaultPolicy = 'deny';
// The trailing comment on the default_policy line, carried so a save puts it back. The block is
// rebuilt from this model, so anything not held here is deleted by the next save.
let _defaultNote = '';
let _activeTab = 'proxies';
// Seeded from PHP rather than hardcoded to 'proxies'. Which panels exist is AUTH_STACK's decision,
// and a stack whose first panel is not Proxies would boot with _activeTab naming a tab that is not
// on the page — so Refresh would reload nothing and the first render would be empty.
let _activeTab = <?= json_encode($first) ?>;
// ── Helpers ───────────────────────────────────────────────────────────────────
function _esc(s) {
@@ -409,6 +462,16 @@ function _post(params, cb) {
// wide is for the one dialog that carries a photo beside a form. Reset on every call rather than
// only set, or a narrow dialog opened after the editor would inherit its width.
// Attach only if the element is there. AUTH_STACK decides which panels the page renders, so on a
// stack that does not carry Users or Access Control those containers genuinely do not exist —
// and a bare getElementById(...).addEventListener would throw on load and take every listener
// after it down with it, including the ones for the panels that do exist.
function _on(id, ev, fn) {
const el = document.getElementById(id);
if (el) el.addEventListener(ev, fn);
return !!el;
}
function _modal(html, wide) {
const m = document.getElementById('vv-au-modal');
m.innerHTML = html;
@@ -474,7 +537,7 @@ document.querySelectorAll('.vv-au-tab').forEach(btn => {
});
});
document.getElementById('vv-au-refresh').addEventListener('click', () => _loadTab(_activeTab));
_on('vv-au-refresh', 'click', () => _loadTab(_activeTab));
function _loadTab(tab) {
if (tab === 'proxies') _loadProxies();
@@ -641,7 +704,7 @@ function _showModalErr(id, msg) {
}
// Proxy event delegation
document.getElementById('vv-au-panel-proxies').addEventListener('click', async e => {
_on('vv-au-panel-proxies', 'click', async e => {
// Add button
if (e.target.id === 'vv-au-proxy-add') { _proxyModal(null); return; }
// Toggle
@@ -1143,7 +1206,7 @@ function _addToGroupModal(uid) {
}
// User event delegation
document.getElementById('vv-au-panel-users').addEventListener('click', async e => {
_on('vv-au-panel-users', 'click', async e => {
if (e.target.id === 'vv-au-user-add') { _userModal(null); return; }
const editBtn = e.target.closest('[data-user-edit]');
@@ -1215,7 +1278,7 @@ function _loadGroups(done) {
}
// Group event delegation
document.getElementById('vv-au-panel-users').addEventListener('click', async e => {
_on('vv-au-panel-users', 'click', async e => {
if (e.target.id === 'vv-au-group-add') {
_modal(`<h3>Add Group</h3>
<div class="vv-au-field">
@@ -1519,7 +1582,7 @@ function _aclMarkDirty(on) {
// Writes straight into the model and deliberately does not re-render: the element being typed in
// is inside the markup a render would replace, which would drop focus on the first keystroke.
document.getElementById('vv-au-panel-acl').addEventListener('input', e => {
_on('vv-au-panel-acl', 'input', e => {
const inp = e.target.closest('.vv-au-dominput');
if (!inp) return;
const r = parseInt(inp.dataset.rule), d = parseInt(inp.dataset.dom);
@@ -1532,7 +1595,7 @@ document.getElementById('vv-au-panel-acl').addEventListener('input', e => {
// Enter adds a row underneath and moves to it, so a list of fourteen can be typed straight
// through instead of returning to the add button between each one.
document.getElementById('vv-au-panel-acl').addEventListener('keydown', e => {
_on('vv-au-panel-acl', 'keydown', e => {
const inp = e.target.closest('.vv-au-dominput');
if (!inp || e.key !== 'Enter') return;
e.preventDefault();
@@ -1550,7 +1613,7 @@ function _focusDomain(r, d) {
}
// ACL event delegation
document.getElementById('vv-au-panel-acl').addEventListener('click', async e => {
_on('vv-au-panel-acl', 'click', async e => {
if (e.target.id === 'vv-au-rule-add') { _ruleModal(null); return; }
const domAdd = e.target.closest('[data-dom-add]');
@@ -1645,10 +1708,10 @@ document.getElementById('vv-au-panel-acl').addEventListener('click', async e =>
// The default policy is the one control here that is not a rule, and it is the most consequential
// one on the tab — it decides what happens to every hostname no rule names.
document.getElementById('vv-au-ac-defpol').addEventListener('change', () => _aclMarkDirty(true));
_on('vv-au-ac-defpol', 'change', () => _aclMarkDirty(true));
// ── Close modal on overlay click ──────────────────────────────────────────────
document.getElementById('vv-au-overlay').addEventListener('click', e => {
_on('vv-au-overlay', 'click', e => {
if (e.target === document.getElementById('vv-au-overlay')) _closeModal();
});
@@ -1722,7 +1785,7 @@ function _loadCerts(onDone) {
});
}
document.getElementById('vv-au-cert-run').addEventListener('click', function() {
_on('vv-au-cert-run', 'click', function() {
const btn = this;
btn.disabled = true; btn.textContent = 'Loading…';
_loadCerts(() => { btn.disabled = false; btn.textContent = 'Refresh'; });
@@ -1750,7 +1813,9 @@ if (document.getElementById('vv-au-ai-chat')) {
}
// ── Boot ──────────────────────────────────────────────────────────────────────
_loadProxies();
// Through _loadTab so the opening panel follows AUTH_STACK, rather than calling _loadProxies()
// directly and reaching into elements a different stack does not render.
_loadTab(_activeTab);
})();
</script>
+26 -2
View File
@@ -807,6 +807,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
| Setting | Control | Where | What it does |
|---|---|---|---|
| `AI_OWNER_HOST` | a text box | in this section | Which node runs the model, the retrieval index and the bug store. Every other node borrows it over the mesh, so a box without a GPU still gets the assistant — it just does not get the AI tab. Defaults to host1 when unset or malformed: whoever builds the mesh is host1. |
| `AI_ENABLED` | a switch | Settings tab | Fail-closed: anything other than the literal "true" means off. |
| `AI_CONNECT_TIMEOUT` | a number box, in seconds | in this section | seconds — probe when resolving which node has Ollama |
| `AI_REQUEST_TIMEOUT` | a number box, in seconds | in this section | seconds — must clear a cold model load |
@@ -1139,6 +1140,19 @@ Saved into `master.conf`, which does not need to be opened by hand.
|---|---|---|---|
| `ARRAY_STOP_SCRIPTS` | a list, one entry per line | in this section | Scripts run by array_stopping.sh for a planned shutdown — stops everything cleanly in order. Run sequentially (foreground) — each must complete before the next starts. Order matters: user scripts first (prevents new ops), then data movement, then containers. |
## Auth Stack
Reachable from:
- Auth tab → Auth settings → *Auth Stack*
- Settings tab → All settings → *Auth Stack*
Saved into `master.conf`, which does not need to be opened by hand.
| Setting | Control | Where | What it does |
|---|---|---|---|
| `AUTH_STACK` | a dropdown (authelia_lldap, authentik) | in this section | Mesh-wide rather than per host: auth is the one service the partnership treats as shared, owned by the mesh owner and consumed by everyone else, so two nodes disagreeing about which stack is in force would mean two different answers to "who is this person". "authelia_lldap" — Authelia for access rules, lldap for users and groups. Fully implemented. "authentik" — single stack for both. NOT IMPLE… |
## Backup Verify
Reachable from:
@@ -1179,7 +1193,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
| Setting | Control | Where | What it does |
|---|---|---|---|
| `BUG_REPORT_LOCAL_ENABLED` | a switch | in this section | LOCAL ON — reports go to your own Gitea (HOST1_BUG_REPORT_* in host1.conf) and stay there. They do NOT reach the Varaverk maintainer. LOCAL OFF — reports open a prefilled GitHub issue you submit under your own account. Nothing is ever transmitted automatically: the report is shown read-only and sending is a separate press. |
| `BUG_REPORT_LOCAL_ENABLED` | a switch | in this section | LOCAL ON — reports go to your own Gitea (see HOSTN_BUG_REPORT_* in host*.conf) and stay there. They do NOT reach the Varaverk maintainer. Turn it on if you want your own backlog. LOCAL OFF — reports open a prefilled GitHub issue you submit under your own account. |
| `BUG_REPORT_GITHUB_REPO` | a text box | in this section | — |
## Certificate Monitor
@@ -1923,6 +1937,16 @@ Saved into `master.conf`, which does not need to be opened by hand.
| `SYS_WATCHDOG_RUNAWAY_STRIKES` | a number box, in consecutive | in this section | consecutive cycles before warning |
| `SYS_WATCHDOG_MDSTAT_ERROR_LIMIT` | a number box | in this section | new errors in one cycle before acting |
## Timed Mutes
Route: Settings tab → All settings → *Timed Mutes*
Saved into `master.conf`, which does not need to be opened by hand.
| Setting | Control | Where | What it does |
|---|---|---|---|
| `WATCHDOG_MUTE_MAX_HOURS` | a number box | in this section | A mute silences one container for a bounded time and then expires on its own. It exists because every other exemption here is permanent — SCAN_IGNORE is a conf edit, an intentional stop lasts until cleared — so a temporary problem gets a permanent workaround that nobody revisits. Capped so "temporary" is enforced rather than intended: a mute longer than this is refused. |
## Transcode Management
Reachable from:
@@ -2090,5 +2114,5 @@ Do not describe a route — there is none, and that is the point.
HOST1 and HOST2 are what detect_hosts() matches the local hostname against. Editing one here would change which machine this believes it is.
- **SHARED HOST CONFIGURATION** (`master.conf`) — `DATA_DIR`, `DB_DIR`, `STATE_DIR`, `AI_DATA_DIR`, `CACHE_BACKUP_DIR`, `LOG_ARCHIVE_DIR`, `BACKUP_DIR`, `CONF_BACKUP_DIR`, `PERSISTENT_CONF_CACHE`, `ARR_CACHE_BACKUP_DIR`
DATA_DIR is the single on-disk root — move it and the whole tree follows, including the state the running scripts are holding open.
- **State Files** (`master.conf`) — `SYS_WATCHDOG_STATE_FILE`, `DOCKER_WATCHDOG_FAILED_FILE`, `DOCKER_WATCHDOG_INTENTIONAL_FILE`, `SYS_WATCHDOG_REBOOT_LOG`, `SYS_WATCHDOG_OOM_FILE`
- **State Files** (`master.conf`) — `SYS_WATCHDOG_STATE_FILE`, `DOCKER_WATCHDOG_FAILED_FILE`, `DOCKER_WATCHDOG_INTENTIONAL_FILE`, `WATCHDOG_MUTE_FILE`, `SYS_WATCHDOG_REBOOT_LOG`, `SYS_WATCHDOG_OOM_FILE`
Paths to live state databases, derived from STATE_DIR. Repointing one orphans the state a watchdog is mid-way through writing.