diff --git a/TODO.md b/TODO.md
index 02273c2..19de142 100644
--- a/TODO.md
+++ b/TODO.md
@@ -166,10 +166,6 @@ Current state on `experimental-okf` as of 2026-06-18: standalone OKF plugin work
- Add examples for fields that expect URLs, model names, provider names, paths, selectors, or structured values.
- Ensure labels and helper text are suitable for non-technical admins without removing important admin-level specificity.
- Review localization/translation keys if present so simplified wording remains consistent across languages.
-- IMPORTANT: on mobile, the navbar extends the viewport vertically, meaning users can only see as far down as "log in with discord". The navbar needs to be the full height of the viewport, and ensure all contained elements are visible. Should contents overflow the viewport, integrate a very slim and minimalistic scrollbar.
-- /plugins/moderation -> User notes should be deletable by admins
-- Theme editor "Popout" button does not create the popout preview as intended.
-- Users not logged in see the browser context menu instead of the lumi-custom context menu.
## Core Feedback System
@@ -585,6 +581,7 @@ Current state on `experimental-feedback-system` as of 2026-06-18: the core feedb
## Done
+- 2026-06-18: Fixed urgent UI tail items locally on `experimental-okf`: mobile sidebar now fits the viewport and scrolls internally, moderation notes can be permanently deleted by admins, both theme preview pop-out buttons open the preview, unauthenticated pages use the Lumi context menu with Feedback disabled, and the global savebar now submits URL-encoded settings data so normal settings routes actually save. No repo push yet.
- 2026-06-18: Updated OKF aliases/related questions to newline-only parsing and display so commas remain valid inside questions. No repo push yet.
- 2026-06-18: Continued `experimental-okf` locally: wired OKF into Lumi AI through the generic context provider hook, made AI context providers user/message-aware, and verified OKF AI context only includes published/approved entries and fields visible to the requester. No repo push yet.
- 2026-06-18: Continued `experimental-okf` locally: added shared core live user lookup, migrated OKF/Lumi AI/Moderation/Economy user-selection fields to it, fixed the moderation target search field, added OKF restore-from-version, and added OKF role-preview cards. No repo push yet.
diff --git a/plugins/moderation/index.js b/plugins/moderation/index.js
index df31cf2..d818f14 100644
--- a/plugins/moderation/index.js
+++ b/plugins/moderation/index.js
@@ -302,6 +302,20 @@ module.exports = {
res.redirect(`/plugins/${PLUGIN_ID}`);
});
+ router.post("/notes/:id/delete", (req, res) => {
+ if (!req.session.user) {
+ return res.redirect("/");
+ }
+ if (!req.session.user?.isAdmin) {
+ return deny(res);
+ }
+ const deleted = deleteNote(db, req.params.id);
+ req.session.flash = deleted
+ ? { type: "success", message: "Note deleted." }
+ : { type: "error", message: "Note not found." };
+ res.redirect(`/plugins/${PLUGIN_ID}`);
+ });
+
web.mount(`/plugins/${PLUGIN_ID}`, router, {
label: "Moderation",
role: "mod",
@@ -636,6 +650,11 @@ function addNote(db, subjectId, note, createdById, createdByName) {
).run(crypto.randomUUID(), subjectId, note, createdById, createdByName, Date.now());
}
+function deleteNote(db, id) {
+ const result = db.prepare("DELETE FROM moderation_notes WHERE id = ?").run(id);
+ return result.changes > 0;
+}
+
function listActions(db, { limit = 200 } = {}) {
return db
.prepare(
diff --git a/plugins/moderation/views/moderation.ejs b/plugins/moderation/views/moderation.ejs
index f676702..b670cf1 100644
--- a/plugins/moderation/views/moderation.ejs
+++ b/plugins/moderation/views/moderation.ejs
@@ -290,6 +290,7 @@
Note |
By |
Date |
+ <% if (isAdmin) { %>Actions | <% } %>
@@ -306,6 +307,13 @@
<%= note.note %> |
<%= note.created_by_name || 'Staff' %> |
<%= new Date(note.created_at).toLocaleString() %> |
+ <% if (isAdmin) { %>
+
+
+ |
+ <% } %>
<% }) %>
diff --git a/src/web/public/app.js b/src/web/public/app.js
index b609c6e..3592438 100644
--- a/src/web/public/app.js
+++ b/src/web/public/app.js
@@ -1640,7 +1640,7 @@
setContextActionEnabled("paste", Boolean(pasteTarget && contextClipboardText.length > 0));
setContextActionEnabled("link", true);
setContextActionEnabled("reload", true);
- setContextActionEnabled("feedback", true);
+ setContextActionEnabled("feedback", !contextMenu.querySelector('[data-context-action="feedback"][data-context-auth-required="true"]'));
};
const showContextMenu = async (event) => {
diff --git a/src/web/public/lumi-interactions.js b/src/web/public/lumi-interactions.js
index 8c6e43e..272f477 100644
--- a/src/web/public/lumi-interactions.js
+++ b/src/web/public/lumi-interactions.js
@@ -114,10 +114,17 @@
status.textContent = "Saving...";
try {
for (const form of forms) {
+ const body = new URLSearchParams();
+ for (const [key, value] of new FormData(form).entries()) {
+ if (typeof value === "string") body.append(key, value);
+ }
const response = await fetch(form.action || window.location.href, {
method: form.method || "POST",
- body: new FormData(form),
- headers: { Accept: "text/html,application/json" },
+ body,
+ headers: {
+ Accept: "text/html,application/json",
+ "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
+ },
redirect: "follow"
});
if (!response.ok) throw new Error(`Save failed for ${form.action || "settings form"}.`);
diff --git a/src/web/public/lumi-layout.css b/src/web/public/lumi-layout.css
index 4b91f01..b023b01 100644
--- a/src/web/public/lumi-layout.css
+++ b/src/web/public/lumi-layout.css
@@ -234,7 +234,22 @@ body {
body.sidebar-collapsed .sidebar,
.sidebar {
width: min(19rem, 88vw);
+ height: 100dvh;
+ max-height: 100dvh;
+ overflow-y: auto;
+ overscroll-behavior: contain;
padding-top: var(--lumi-space-3);
+ scrollbar-width: thin;
+ scrollbar-color: var(--lumi-border) transparent;
+ }
+
+ .sidebar-nav {
+ min-height: 0;
+ overflow-y: visible;
+ }
+
+ .sidebar-footer {
+ flex-shrink: 0;
}
.sidebar-brand {
diff --git a/src/web/public/styles.css b/src/web/public/styles.css
index 9486f90..9d41cce 100644
--- a/src/web/public/styles.css
+++ b/src/web/public/styles.css
@@ -1930,7 +1930,11 @@ body .modal-backdrop.destructive-confirm-modal {
top: 0;
left: 0;
bottom: 0;
+ height: 100dvh;
+ max-height: 100dvh;
width: 260px;
+ overflow-y: auto;
+ overscroll-behavior: contain;
transform: translateX(-100%);
transition: transform 0.2s ease;
z-index: 20;
diff --git a/src/web/public/theme-editor.js b/src/web/public/theme-editor.js
index 9c2deec..a93dba8 100644
--- a/src/web/public/theme-editor.js
+++ b/src/web/public/theme-editor.js
@@ -252,18 +252,21 @@
applyPreview();
});
- editor.querySelector("[data-theme-popout]")?.addEventListener("click", () => {
- popout = window.open("", "lumi-theme-preview", "width=430,height=760,menubar=no,toolbar=no,location=no,status=no");
- const status = editor.querySelector("[data-theme-popout-status]");
- if (!popout) {
- if (status) status.textContent = "Pop-out preview was blocked by the browser.";
- return;
- }
- popout.document.open();
- popout.document.write(previewShell());
- popout.document.close();
- if (status) status.textContent = "Pop-out preview is open and updates with this editor.";
- syncPopout();
+ editor.querySelectorAll("[data-theme-popout]").forEach((button) => {
+ button.addEventListener("click", () => {
+ popout = window.open("", "lumi-theme-preview", "width=430,height=760,menubar=no,toolbar=no,location=no,status=no");
+ const status = editor.querySelector("[data-theme-popout-status]");
+ if (!popout) {
+ if (status) status.textContent = "Pop-out preview was blocked by the browser.";
+ return;
+ }
+ popout.document.open();
+ popout.document.write(previewShell());
+ popout.document.close();
+ popout.focus?.();
+ if (status) status.textContent = "Pop-out preview is open and updates with this editor.";
+ syncPopout();
+ });
});
applyPreview();
diff --git a/src/web/views/partials/layout-bottom.ejs b/src/web/views/partials/layout-bottom.ejs
index 622555b..18fc247 100644
--- a/src/web/views/partials/layout-bottom.ejs
+++ b/src/web/views/partials/layout-bottom.ejs
@@ -125,6 +125,7 @@
+ <% } %>
- <% } %>