Fixed admin display.
This commit is contained in:
parent
0177cfaef8
commit
bcca593137
1 changed files with 59 additions and 53 deletions
112
lib/routes.js
112
lib/routes.js
|
|
@ -71,54 +71,60 @@ const csrfField = (req) => {
|
|||
};
|
||||
|
||||
|
||||
const layout = (title, body, flash) => `<!doctype html>
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8">
|
||||
<title>${escape(title)}</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, sans-serif; margin: 1.5rem; max-width: 1100px; }
|
||||
h1 { font-size: 1.4rem; }
|
||||
h2 { font-size: 1.1rem; margin-top: 1.5rem; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 0.85rem; }
|
||||
th, td { border: 1px solid #ddd; padding: 0.3rem 0.5rem; text-align: left; vertical-align: top; }
|
||||
th { background: #f5f5f5; }
|
||||
tr:nth-child(even) td { background: #fafafa; }
|
||||
code, pre { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 0.8rem; }
|
||||
pre { background: #f5f5f5; padding: 0.5rem; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }
|
||||
// Bootstrap nav-pills with the current page marked active. The active link is the
|
||||
// one whose href is the LONGEST boundary-safe prefix of the request path, so a
|
||||
// sub-page (e.g. /conflicts/merge) still highlights its parent tab.
|
||||
const navPills = (req, links) => {
|
||||
const cur = String((req && (req.originalUrl || req.path)) || "").split("?")[0].replace(/\/+$/, "");
|
||||
let activeHref = "";
|
||||
for (const [href] of links) {
|
||||
const h = href.replace(/\/+$/, "");
|
||||
if ((cur === h || cur.startsWith(h + "/")) && h.length > activeHref.length) {
|
||||
activeHref = h;
|
||||
}
|
||||
}
|
||||
const items = links.map(([href, label]) => {
|
||||
const on = href.replace(/\/+$/, "") === activeHref;
|
||||
return ` <li class="nav-item"><a class="nav-link${on ? " active" : ""}"${on ? ' aria-current="page"' : ""} href="${escape(href)}">${escape(label)}</a></li>`;
|
||||
}).join("\n");
|
||||
return `<ul class="nav nav-pills mb-3">\n${items}\n</ul>`;
|
||||
};
|
||||
|
||||
|
||||
// Returns the INNER page content only (Bootstrap sub-nav + a small residual
|
||||
// style for the non-table classes the body markup uses + optional flash + body).
|
||||
// The page is wrapped in the active Saltcorn theme by res.sendWrap at the call
|
||||
// sites, so there is no <html>/<head>/<body> here.
|
||||
const layout = (req, body, flash) => `<style>
|
||||
.muted { color: #888; }
|
||||
.pill { display: inline-block; padding: 0.05rem 0.5rem; border-radius: 0.5rem; background: #eef; font-size: 0.75rem; }
|
||||
nav { margin-bottom: 1rem; }
|
||||
nav a { margin-right: 1rem; }
|
||||
.right { float: right; color: #888; font-size: 0.85rem; }
|
||||
code, pre { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 0.8rem; }
|
||||
pre { background: #f5f5f5; padding: 0.5rem; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }
|
||||
form.inline { display: inline; }
|
||||
input[type=text], input[type=url] { width: 28rem; padding: 0.2rem; }
|
||||
fieldset { margin-bottom: 1rem; padding: 0.5rem 1rem; }
|
||||
legend { font-weight: bold; }
|
||||
.flash { padding: 0.5rem 1rem; margin-bottom: 1rem; background: #efe; border: 1px solid #cdc; border-radius: 0.3rem; }
|
||||
.flash.err { background: #fee; border-color: #fcc; }
|
||||
.secret { font-family: ui-monospace, Menlo, Consolas, monospace; background: #fff8dc; padding: 0.5rem; border: 1px solid #d4af37; border-radius: 0.3rem; word-break: break-all; }
|
||||
button { padding: 0.25rem 0.6rem; cursor: pointer; }
|
||||
button:not(.btn) { padding: 0.25rem 0.6rem; cursor: pointer; }
|
||||
</style>
|
||||
</head><body>
|
||||
<nav>
|
||||
<a href="/admin/dev-deploy/">Dashboard</a>
|
||||
<a href="/admin/dev-deploy/ops">Journal</a>
|
||||
<a href="/admin/dev-deploy/peers">Peers</a>
|
||||
<a href="/admin/dev-deploy/plan">Plan</a>
|
||||
<a href="/admin/dev-deploy/tables">Tables</a>
|
||||
<a href="/admin/dev-deploy/conflicts">Conflicts</a>
|
||||
<span class="right">${escape(PLUGIN_NAME)} v${escape(PLUGIN_VERSION)}</span>
|
||||
</nav>
|
||||
${navPills(req, [
|
||||
["/admin/dev-deploy/", "Dashboard"],
|
||||
["/admin/dev-deploy/ops", "Journal"],
|
||||
["/admin/dev-deploy/peers", "Peers"],
|
||||
["/admin/dev-deploy/plan", "Plan"],
|
||||
["/admin/dev-deploy/tables", "Tables"],
|
||||
["/admin/dev-deploy/conflicts", "Conflicts"],
|
||||
])}
|
||||
${flash || ""}
|
||||
${body}
|
||||
</body></html>`;
|
||||
<p class="text-muted small mt-3">${escape(PLUGIN_NAME)} v${escape(PLUGIN_VERSION)}</p>`;
|
||||
|
||||
|
||||
const flashMsg = (req) => {
|
||||
const m = req.query.msg;
|
||||
const e = req.query.err;
|
||||
if (m) return `<div class="flash">${escape(m)}</div>`;
|
||||
if (e) return `<div class="flash err">${escape(e)}</div>`;
|
||||
if (m) return `<div class="alert alert-success">${escape(m)}</div>`;
|
||||
if (e) return `<div class="alert alert-danger">${escape(e)}</div>`;
|
||||
return "";
|
||||
};
|
||||
|
||||
|
|
@ -145,7 +151,7 @@ const dashboard = async (req, res) => {
|
|||
|
||||
const body = `
|
||||
<h1>dev-deploy dashboard</h1>
|
||||
<table>
|
||||
<table class="table table-sm table-bordered">
|
||||
<tr><th>Env ID</th><td><code>${escape(env ? env.env_id : "?")}</code></td></tr>
|
||||
<tr><th>Label</th><td>${env && env.env_label ? escape(env.env_label) : '<span class="muted">(unset)</span>'}</td></tr>
|
||||
<tr><th>Destructive-op policy</th><td><span class="pill">${escape(env ? env.on_destructive_op : "?")}</span></td></tr>
|
||||
|
|
@ -156,11 +162,11 @@ const dashboard = async (req, res) => {
|
|||
<tr><th>Pending conflicts</th><td>${conflictCount > 0 ? `<a href="/admin/dev-deploy/conflicts"><strong>${escape(conflictCount)}</strong></a>` : "0"}</td></tr>
|
||||
</table>
|
||||
<h2>Ops by type</h2>
|
||||
<table><tr><th>op_type</th><th>count</th></tr>${opsByKindRows}</table>
|
||||
<table class="table table-sm table-bordered"><tr><th>op_type</th><th>count</th></tr>${opsByKindRows}</table>
|
||||
<h2>Entities tracked</h2>
|
||||
<table><tr><th>kind</th><th>count</th></tr>${entRows}</table>
|
||||
<table class="table table-sm table-bordered"><tr><th>kind</th><th>count</th></tr>${entRows}</table>
|
||||
`;
|
||||
res.type("text/html").send(layout("dev-deploy dashboard", body, flashMsg(req)));
|
||||
res.sendWrap("dev-deploy dashboard", layout(req, body, flashMsg(req)));
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -225,13 +231,13 @@ const opsView = async (req, res) => {
|
|||
const body = `
|
||||
<h1>Journal</h1>
|
||||
<p class="muted">Showing up to ${escape(limit)} ops${since ? `, since op <code>${escape(since.slice(0, 8))}</code>` : ""}, offset ${escape(offset)}. Newest first. Revert appends a compensating op rather than rewriting history.</p>
|
||||
<table>
|
||||
<table class="table table-sm table-bordered">
|
||||
<tr><th>op</th><th>op_type</th><th>entity</th><th>parent</th><th>status</th><th>created</th><th>actions</th><th>payload</th></tr>
|
||||
${rows}
|
||||
</table>
|
||||
<p>${prevLink} ${nextLink}</p>
|
||||
`;
|
||||
res.type("text/html").send(layout("dev-deploy journal", body, flashMsg(req)));
|
||||
res.sendWrap("dev-deploy journal", layout(req, body, flashMsg(req)));
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -276,7 +282,7 @@ const peersView = async (req, res) => {
|
|||
const body = `
|
||||
<h1>Peers</h1>
|
||||
<p>This instance's <strong>env_id</strong> is <code>${escape(env ? env.env_id : "?")}</code>. Paste this into the other instance's peer form.</p>
|
||||
<table>
|
||||
<table class="table table-sm table-bordered">
|
||||
<tr><th>id</th><th>label</th><th>env_id</th><th>base_url</th><th>last seen</th><th>actions</th></tr>
|
||||
${rows}
|
||||
</table>
|
||||
|
|
@ -299,7 +305,7 @@ const peersView = async (req, res) => {
|
|||
<p><button type="submit">Pair</button></p>
|
||||
</form>
|
||||
`;
|
||||
res.type("text/html").send(layout("dev-deploy peers", body, flashMsg(req)));
|
||||
res.sendWrap("dev-deploy peers", layout(req, body, flashMsg(req)));
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -327,7 +333,7 @@ const peersAdd = async (req, res) => {
|
|||
<p>If this is a brand-new pairing, give the peer this side's env_id too.</p>
|
||||
<p><a href="/admin/dev-deploy/peers">Back to peers</a></p>
|
||||
`;
|
||||
res.type("text/html").send(layout("Peer paired", body));
|
||||
res.sendWrap("Peer paired", layout(req, body));
|
||||
} catch (err) {
|
||||
res.redirect("/admin/dev-deploy/peers?err=" + encodeURIComponent(err.message));
|
||||
}
|
||||
|
|
@ -346,7 +352,7 @@ const peersRotate = async (req, res) => {
|
|||
<p>Paste this on the other side via Rotate or by re-pairing.</p>
|
||||
<p><a href="/admin/dev-deploy/peers">Back to peers</a></p>
|
||||
`;
|
||||
res.type("text/html").send(layout("Secret rotated", body));
|
||||
res.sendWrap("Secret rotated", layout(req, body));
|
||||
} catch (err) {
|
||||
res.redirect("/admin/dev-deploy/peers?err=" + encodeURIComponent(err.message));
|
||||
}
|
||||
|
|
@ -382,7 +388,7 @@ const planView = async (req, res) => {
|
|||
<button type="submit">Show plan</button>
|
||||
</form>
|
||||
`;
|
||||
res.type("text/html").send(layout("dev-deploy plan", body, flashMsg(req)));
|
||||
res.sendWrap("dev-deploy plan", layout(req, body, flashMsg(req)));
|
||||
return;
|
||||
}
|
||||
const peerId = parseInt(peerIdRaw, 10);
|
||||
|
|
@ -421,7 +427,7 @@ const planView = async (req, res) => {
|
|||
<h1>Plan: promote to ${escape(peer.label || peer.env_id)}</h1>
|
||||
<p>Anchor: ${anchor ? `<code>${escape(anchor.last_op_id.slice(0, 8))}</code>` : '<span class="muted">(none — will send from epoch)</span>'}</p>
|
||||
<p>Ops that would be sent: ${escape(planRows.length)}</p>
|
||||
<table>
|
||||
<table class="table table-sm table-bordered">
|
||||
<tr><th>op</th><th>op_type</th><th>entity</th><th>status</th><th>created</th></tr>
|
||||
${rowsHtml}
|
||||
</table>
|
||||
|
|
@ -431,7 +437,7 @@ const planView = async (req, res) => {
|
|||
<p><button type="submit"${planRows.length === 0 ? " disabled" : ""}>Promote ${escape(planRows.length)} op${planRows.length === 1 ? "" : "s"} to ${escape(peer.label || peer.env_id)}</button></p>
|
||||
</form>
|
||||
`;
|
||||
res.type("text/html").send(layout("dev-deploy plan", body, flashMsg(req)));
|
||||
res.sendWrap("dev-deploy plan", layout(req, body, flashMsg(req)));
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -628,12 +634,12 @@ const conflictsView = async (req, res) => {
|
|||
<li><strong>Use theirs</strong>: applies the incoming op now (overwrites local change). The local op stays in the journal but its effect is overwritten.</li>
|
||||
<li><strong>Use mine</strong>: marks the incoming op as <code>rejected</code>. The local state stands. The peer may re-send the op on future syncs; subsequent pulls will skip it via idempotency.</li>
|
||||
</ul>
|
||||
<table>
|
||||
<table class="table table-sm table-bordered">
|
||||
<tr><th style="width: 40%">Theirs</th><th style="width: 40%">Mine</th><th>Action</th></tr>
|
||||
${rowsHtml}
|
||||
</table>
|
||||
`;
|
||||
res.type("text/html").send(layout("dev-deploy conflicts", body, flashMsg(req)));
|
||||
res.sendWrap("dev-deploy conflicts", layout(req, body, flashMsg(req)));
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -686,7 +692,7 @@ const conflictsMergeView = async (req, res) => {
|
|||
|
||||
const ent = diff.instance
|
||||
? `<p>Entity: <code>${escape(diff.kind)}</code> <strong>${escape(diff.instance.name || diff.instance.role || diff.instance.id)}</strong> (local id ${escape(diff.instance.id)})</p>`
|
||||
: `<p class="flash err">${escape(diff.reason || "no entity diff available")}</p>`;
|
||||
: `<p class="alert alert-danger">${escape(diff.reason || "no entity diff available")}</p>`;
|
||||
|
||||
let formBody;
|
||||
if (diffs.length === 0) {
|
||||
|
|
@ -705,7 +711,7 @@ const conflictsMergeView = async (req, res) => {
|
|||
</td>
|
||||
</tr>`).join("");
|
||||
formBody = `
|
||||
<table>
|
||||
<table class="table table-sm table-bordered">
|
||||
<tr><th>field</th><th>current (mine)</th><th>incoming (theirs)</th><th>resolution</th></tr>
|
||||
${rows}
|
||||
</table>
|
||||
|
|
@ -727,7 +733,7 @@ const conflictsMergeView = async (req, res) => {
|
|||
</p>
|
||||
</form>
|
||||
`;
|
||||
res.type("text/html").send(layout("dev-deploy merge", body, flashMsg(req)));
|
||||
res.sendWrap("dev-deploy merge", layout(req, body, flashMsg(req)));
|
||||
} catch (err) {
|
||||
res.redirect("/admin/dev-deploy/conflicts?err=" + encodeURIComponent(err.message));
|
||||
}
|
||||
|
|
@ -834,12 +840,12 @@ const tablesView = async (req, res) => {
|
|||
<li><strong>managed</strong> — rows always sync from source. Source is canonical; target's edits get overwritten or surface as conflicts. Good for catalogs, lookup tables, anything dev-curated.</li>
|
||||
</ul>
|
||||
<p>The Saltcorn <code>users</code> table is locked to <strong>user</strong> and cannot be changed.</p>
|
||||
<table>
|
||||
<table class="table table-sm table-bordered">
|
||||
<tr><th>table</th><th>uuid</th><th>local id</th><th>data_mode</th><th>updated_at</th></tr>
|
||||
${rowsHtml}
|
||||
</table>
|
||||
`;
|
||||
res.type("text/html").send(layout("dev-deploy tables", body, flashMsg(req)));
|
||||
res.sendWrap("dev-deploy tables", layout(req, body, flashMsg(req)));
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue