diff --git a/TODO.md b/TODO.md index 19c8dee..db34850 100644 --- a/TODO.md +++ b/TODO.md @@ -4,7 +4,7 @@ This file tracks larger Lumi work that cannot safely be completed in one pass. K ## OKF Knowledge System -Current state on `experimental-okf` as of 2026-06-18: first basic standalone OKF plugin pass is implemented locally and not pushed. The plugin provides SQLite-backed entries, sanitized Markdown, logged-in browsing, role-gated details, admin/editor management, per-user OKF permission grants, workflow actions, and version history. File-backed OKF directories, indexing, AI prompt integration, and feedback-to-correction flow remain for later passes. +Current state on `experimental-okf` as of 2026-06-18: standalone OKF plugin work is implemented locally and not pushed. The plugin provides SQLite-backed entries, sanitized Markdown, logged-in browsing, role-gated details, admin/editor management, per-user OKF permission grants through the shared core user lookup, workflow actions, version history, version restore, and role-preview tooling. File-backed OKF directories, indexing, AI prompt integration, and feedback-to-correction flow remain for later passes. ### Implemented Locally @@ -24,6 +24,11 @@ Current state on `experimental-okf` as of 2026-06-18: first basic standalone OKF - `edit_review_implement` - Added workflow actions for review, publish, archive, restore-as-draft, and soft delete. - Added version snapshots for create/update/workflow changes. +- Added restore-from-version service/action/UI with a new audit version recorded for restores. +- Added admin role preview cards for user, moderator, and admin/editor visibility. +- Added shared core user lookup support through `/api/users/search` and `[data-user-lookup]`. +- Migrated OKF permission grants, Lumi AI access grants, moderation target fields, Economy banking transfers, and Economy balance adjustment fields to the shared user lookup. +- Fixed the moderation target internal username search by removing its page-local picker and using the shared live lookup. - Added a local `global.lumiFrameworks.okf.search(...)` integration point for later AI context provider wiring. - Added `plugins/okf/tests/verify.js`. @@ -36,8 +41,7 @@ Current state on `experimental-okf` as of 2026-06-18: first basic standalone OKF - Add changed-file reindexing where practical. - Enforce retrieval priority for corrections, community OKF, plugin OKF, and core OKF. - Add safe placeholder resolution for generated OKF references. -- Add restore-from-version UI/action. -- Add richer category/tag management and role-preview tools. +- Add richer category/tag management and Markdown preview/editor polish. - Add correction-file creation from reviewed feedback after file-backed OKF storage exists. - Add update preservation tests/notes for OKF file paths once those paths exist. @@ -160,6 +164,7 @@ Current state on `experimental-okf` as of 2026-06-18: first basic standalone OKF - 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 ## Core Feedback System @@ -575,6 +580,7 @@ Current state on `experimental-feedback-system` as of 2026-06-18: the core feedb ## Done +- 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. - 2026-06-18: Started `experimental-okf` with a standalone OKF plugin: role-gated SQLite entries, sanitized Markdown browsing, admin/editor management, per-user OKF grants, workflow actions, version snapshots, and first verification coverage. No repo push yet. - 2026-06-18: Emergency patched `/admin/navigation` so the Unassigned items pool stays sticky while editing sections on desktop, and aligned Save navigation / Reset to default actions in a shared horizontal Lumi button group. - 2026-06-18: Removed feedback conversion actions from the experimental feedback system, added Finalize & Close/Reopen/Delete admin actions, made delete hard-remove feedback records/comments/notes/history/screenshots, kept admin feedback rows collapsed by default, and replaced browser tab capture screenshots with upload/clipboard-paste attachments. diff --git a/plugins/economy-framework/index.js b/plugins/economy-framework/index.js index fccbe0d..e1094fa 100644 --- a/plugins/economy-framework/index.js +++ b/plugins/economy-framework/index.js @@ -495,16 +495,19 @@ module.exports = { if (!req.session.user || !req.session.user.isMod) { return deny(res); } + const targetUserId = (req.body.target_user_id || "").trim(); const targetName = (req.body.username || "").trim(); const amount = parseSignedAmount(req.body.amount); - if (!targetName || !Number.isFinite(amount)) { + if ((!targetUserId && !targetName) || !Number.isFinite(amount)) { req.session.flash = { type: "error", message: "Username and amount are required." }; return res.redirect(`/plugins/${PLUGIN_ID}`); } - const target = findUserByInternalName(db, targetName); + const target = targetUserId + ? db.prepare("SELECT id, internal_username FROM user_profiles WHERE id = ?").get(targetUserId) + : findUserByInternalName(db, targetName); if (!target) { req.session.flash = { type: "error", message: "User not found." }; return res.redirect(`/plugins/${PLUGIN_ID}`); @@ -626,16 +629,13 @@ module.exports = { limit: 1000 }); const funds = listFunds(db).filter((fund) => fund.status === "active"); - const userDirectory = listUserDirectory(db); - res.render(path.join(__dirname, "views", "banking.ejs"), { title: config.banking.label, config, user, userStats, transactions, - funds, - userDirectory + funds }); }); @@ -647,7 +647,7 @@ module.exports = { const targetName = (req.body.username || "").trim(); const amount = parseAmount(req.body.amount); const note = (req.body.note || "").trim(); - if (!targetName || !Number.isFinite(amount)) { + if ((!targetUserId && !targetName) || !Number.isFinite(amount)) { req.session.flash = { type: "error", message: "Recipient and amount are required." @@ -662,7 +662,9 @@ module.exports = { }; return res.redirect("/profile/banking"); } - const target = findUserByInternalName(db, targetName.replace(/^@/, "")); + const target = targetUserId + ? db.prepare("SELECT id, internal_username FROM user_profiles WHERE id = ?").get(targetUserId) + : findUserByInternalName(db, targetName.replace(/^@/, "")); if (!target) { req.session.flash = { type: "error", message: "User not found." }; return res.redirect("/profile/banking"); @@ -2462,57 +2464,6 @@ function listFunds(db) { .all(); } -function formatProviderLabel(provider) { - const normalized = (provider || "").toLowerCase(); - const map = { - discord: "Discord", - twitch: "Twitch", - twitch_login: "Twitch", - youtube: "YouTube", - youtube_name: "YouTube", - economy_name: "Internal" - }; - if (map[normalized]) { - return map[normalized]; - } - if (!normalized) { - return "Account"; - } - return normalized.charAt(0).toUpperCase() + normalized.slice(1); -} - -function listUserDirectory(db) { - const rows = db - .prepare( - "SELECT user_profiles.id AS user_id, user_profiles.internal_username AS internal_username, " + - "user_identities.provider AS provider, user_identities.display_name AS display_name, " + - "user_identities.provider_user_id AS provider_user_id " + - "FROM user_profiles " + - "LEFT JOIN user_identities ON user_identities.user_id = user_profiles.id " + - "ORDER BY user_profiles.internal_username" - ) - .all(); - const map = new Map(); - rows.forEach((row) => { - if (!map.has(row.user_id)) { - map.set(row.user_id, { - id: row.user_id, - internal: row.internal_username || "", - identities: [] - }); - } - if (row.provider) { - const display = row.display_name || row.provider_user_id || ""; - map.get(row.user_id).identities.push({ - provider: row.provider, - label: formatProviderLabel(row.provider), - display - }); - } - }); - return Array.from(map.values()); -} - function findFund(db, name) { return db .prepare("SELECT * FROM economy_pots WHERE lower(name) = lower(?)") diff --git a/plugins/economy-framework/views/banking.ejs b/plugins/economy-framework/views/banking.ejs index 1ee4525..8c66bb3 100644 --- a/plugins/economy-framework/views/banking.ejs +++ b/plugins/economy-framework/views/banking.ejs @@ -67,73 +67,6 @@ .uuid-chip:hover { border-color: var(--ink-soft); } - .user-search { - display: grid; - gap: 10px; - } - .user-results { - display: grid; - gap: 6px; - } - .user-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - padding: 6px 10px; - border-radius: 12px; - border: 1px solid var(--border); - background: var(--surface-2); - } - .user-row.is-selected { - border-color: var(--accent); - } - .user-main { - display: flex; - align-items: center; - gap: 10px; - flex-wrap: wrap; - } - .user-name { - font-weight: 600; - } - .user-pills { - display: flex; - gap: 6px; - flex-wrap: wrap; - } - .user-pill { - font-size: 0.7rem; - padding: 2px 6px; - border-radius: 999px; - background: var(--surface-3); - color: var(--ink-soft); - } - .user-expand { - background: transparent; - border: none; - color: var(--ink-soft); - cursor: pointer; - font-size: 0.75rem; - } - .user-details { - display: none; - font-size: 0.78rem; - color: var(--ink-soft); - margin-top: 4px; - } - .user-row.is-open .user-details { - display: inline; - } - .user-select { - background: var(--accent); - color: var(--accent-ink); - border: none; - border-radius: 999px; - padding: 4px 10px; - cursor: pointer; - font-size: 0.75rem; - } .tx-note-details { margin: 0; } @@ -199,11 +132,13 @@

Transfer to another user

-
- - <%- include("../../../src/web/views/partials/layout-bottom") %> diff --git a/plugins/economy-framework/views/economy.ejs b/plugins/economy-framework/views/economy.ejs index 522a2b5..f0eaa1f 100644 --- a/plugins/economy-framework/views/economy.ejs +++ b/plugins/economy-framework/views/economy.ejs @@ -537,9 +537,12 @@

Adjust user balance

-
+
- + + + +
diff --git a/plugins/lumi_ai/backend/repo_indexer.js b/plugins/lumi_ai/backend/repo_indexer.js index fd29954..ef53e33 100644 --- a/plugins/lumi_ai/backend/repo_indexer.js +++ b/plugins/lumi_ai/backend/repo_indexer.js @@ -266,7 +266,13 @@ function scoreRoute(route, terms) { } function augmentedRoutes(index) { - return [...(index?.routes || [])].filter((route) => route.method === "GET"); + return [...(index?.routes || [])].filter(isUserFacingRoute); +} + +function isUserFacingRoute(route) { + if (!route || route.method !== "GET" || typeof route.path !== "string") return false; + if (!route.path.startsWith("/")) return false; + return !route.path.startsWith("/api/") && !route.path.includes("/api/"); } function verifiedRoutePaths(index = loadIndex()) { diff --git a/plugins/lumi_ai/index.js b/plugins/lumi_ai/index.js index 90d4f24..66172f4 100644 --- a/plugins/lumi_ai/index.js +++ b/plugins/lumi_ai/index.js @@ -728,12 +728,6 @@ module.exports = { return flash(req, res, "error", error.message); } }); - router.get("/api/users/search", (req, res) => { - if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); - const query = cleanText(req.query.q, 120); - if (query.length < 2) return res.json({ users: [] }); - return res.json({ users: searchKnownUsers(db, query) }); - }); router.post("/models/:id/delete", (req, res) => { if (!req.session.user?.isAdmin) return denied(res); try { diff --git a/plugins/lumi_ai/public/settings.css b/plugins/lumi_ai/public/settings.css index 34d9cc0..f1fde53 100644 --- a/plugins/lumi_ai/public/settings.css +++ b/plugins/lumi_ai/public/settings.css @@ -69,10 +69,6 @@ .ai-access-form { display: grid; grid-template-columns: minmax(260px, 420px) 170px 220px; align-items: end; gap: 12px; max-width: 920px; margin-top: 16px; } .ai-access-form .field { min-width: 0; } .ai-user-picker { position: relative; grid-column: span 2; } -.ai-user-results { position: absolute; z-index: 8; top: 100%; left: 0; right: 0; max-height: 240px; overflow: auto; border: 1px solid var(--border); background: var(--card); box-shadow: 0 8px 22px rgb(0 0 0 / 16%); } -.ai-user-result { display: block; width: 100%; padding: 9px 10px; border: 0; border-bottom: 1px solid var(--border); background: transparent; color: var(--ink); text-align: left; cursor: pointer; } -.ai-user-result:hover, .ai-user-result:focus { background: var(--surface-2); } -.ai-user-preview { margin-top: 6px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface-2); color: var(--ink-soft); font-size: 12px; overflow-wrap: anywhere; } .ai-fieldset { display: flex; flex-wrap: wrap; gap: 10px 20px; margin: 0; padding: 12px; border: 1px solid var(--border); border-radius: 7px; } .ai-fieldset legend { padding: 0 5px; font-weight: 700; } .ai-fieldset label { display: flex; align-items: center; gap: 6px; } diff --git a/plugins/lumi_ai/public/settings.js b/plugins/lumi_ai/public/settings.js index 6e41732..7c33b4c 100644 --- a/plugins/lumi_ai/public/settings.js +++ b/plugins/lumi_ai/public/settings.js @@ -215,13 +215,8 @@ refreshCapacity(); } if (accessForm) { - const search = accessForm.querySelector("[data-user-search]"); - const userId = accessForm.querySelector("[data-user-id]"); - const results = accessForm.querySelector("[data-user-results]"); - const preview = accessForm.querySelector("[data-user-preview]"); const action = accessForm.querySelector("[data-access-action]"); const timeoutField = accessForm.querySelector("[data-timeout-field]"); - let searchTimer = null; const updateTimeoutVisibility = () => { const visible = action.value === "timeout"; @@ -230,57 +225,6 @@ input.required = visible; if (!visible) input.value = ""; }; - const selectUser = (user) => { - userId.value = user.id; - search.value = user.username; - const identities = user.identities.map((identity) => - `${identity.provider}: ${identity.display_name || identity.provider_user_id}`).join(" | "); - preview.textContent = `${user.username} | ${user.id}${identities ? ` | ${identities}` : ""}`; - preview.hidden = false; - results.hidden = true; - }; - const renderUsers = (users) => { - results.replaceChildren(); - for (const user of users) { - const button = document.createElement("button"); - button.type = "button"; - button.className = "ai-user-result"; - const identity = user.identities[0]; - button.textContent = identity - ? `${user.username} | ${identity.display_name || identity.provider_user_id} (${identity.provider})` - : `${user.username} | ${user.id}`; - button.addEventListener("click", () => selectUser(user)); - results.append(button); - } - results.hidden = !users.length; - }; - search.addEventListener("input", () => { - userId.value = ""; - preview.hidden = true; - window.clearTimeout(searchTimer); - const query = search.value.trim(); - if (query.length < 2) { - results.hidden = true; - return; - } - searchTimer = window.setTimeout(async () => { - try { - const response = await fetch(`/plugins/lumi_ai/api/users/search?q=${encodeURIComponent(query)}`, { cache: "no-store" }); - const data = await response.json(); - renderUsers(response.ok ? data.users || [] : []); - } catch { - renderUsers([]); - } - }, 180); - }); - accessForm.addEventListener("submit", (event) => { - if (!userId.value) { - event.preventDefault(); - search.setCustomValidity("Select a known Lumi user."); - search.reportValidity(); - } - }); - search.addEventListener("input", () => search.setCustomValidity("")); action.addEventListener("change", updateTimeoutVisibility); updateTimeoutVisibility(); } diff --git a/plugins/lumi_ai/tests/verify.js b/plugins/lumi_ai/tests/verify.js index c9b873f..0d28ee8 100644 --- a/plugins/lumi_ai/tests/verify.js +++ b/plugins/lumi_ai/tests/verify.js @@ -1369,7 +1369,8 @@ async function run() { const assistantStyles = fs.readFileSync(require("path").join(PLUGIN_ROOT, "public", "assistant.css"), "utf8"); const assistantPanel = fs.readFileSync(require("path").join(PLUGIN_ROOT, "views", "assistant-panel.ejs"), "utf8"); const pluginIndex = fs.readFileSync(require("path").join(PLUGIN_ROOT, "index.js"), "utf8"); - assert(accessSettingsTemplate.includes("data-user-search")); + assert(accessSettingsTemplate.includes("data-user-lookup")); + assert(accessSettingsTemplate.includes("data-user-lookup-search")); assert(accessSettingsTemplate.includes("data-timeout-field hidden")); assert(settingsScript.includes('action.value === "timeout"')); assert(assistantScript.includes("window.innerHeight / 6")); diff --git a/plugins/lumi_ai/views/settings.ejs b/plugins/lumi_ai/views/settings.ejs index 80835f1..3317051 100644 --- a/plugins/lumi_ai/views/settings.ejs +++ b/plugins/lumi_ai/views/settings.ejs @@ -498,12 +498,12 @@

User AI access

Bans and timeouts apply to WebUI and platform commands.

-
+
- - - - + + + +
diff --git a/plugins/moderation/index.js b/plugins/moderation/index.js index 4932110..df31cf2 100644 --- a/plugins/moderation/index.js +++ b/plugins/moderation/index.js @@ -81,7 +81,6 @@ module.exports = { } const isAdmin = Boolean(req.session.user?.isAdmin); const isMod = Boolean(req.session.user?.isAdmin || req.session.user?.isMod); - const userDirectory = listUserDirectory(db); const actions = listActions(db, { limit: 500 }); const actionEvidence = listEvidenceForActions( db, @@ -94,7 +93,6 @@ module.exports = { title: "Moderation Center", isAdmin, isMod, - userDirectory, actions, actionEvidence, notes, @@ -409,29 +407,18 @@ function ensureBanPot(db) { } } -function listUserDirectory(db) { - const users = db.prepare("SELECT id, internal_username FROM user_profiles ORDER BY internal_username").all(); - const identities = db - .prepare("SELECT user_id, provider, provider_user_id, display_name FROM user_identities") - .all(); - const map = new Map(); - users.forEach((user) => { - map.set(user.id, { id: user.id, internal: user.internal_username, identities: [] }); - }); - identities.forEach((row) => { - if (!map.has(row.user_id)) { - map.set(row.user_id, { id: row.user_id, internal: row.user_id, identities: [] }); - } - map.get(row.user_id).identities.push({ - label: row.provider, - id: row.provider_user_id, - display: row.display_name || row.provider_user_id - }); - }); - return Array.from(map.values()); -} - function resolveTarget(db, body) { + const selectedUserId = (body.target_user_id || "").trim(); + if (selectedUserId) { + const user = db + .prepare("SELECT id, internal_username FROM user_profiles WHERE id = ?") + .get(selectedUserId); + if (user) { + const subjectId = getOrCreateSubjectByUser(db, user.id, user.internal_username); + syncSubjectIdentities(db, subjectId, user.id); + return { subjectId, internalUserId: user.id }; + } + } const internal = (body.target_username || "").trim(); if (internal) { const user = db diff --git a/plugins/moderation/views/moderation.ejs b/plugins/moderation/views/moderation.ejs index 7200123..f676702 100644 --- a/plugins/moderation/views/moderation.ejs +++ b/plugins/moderation/views/moderation.ejs @@ -117,73 +117,6 @@ .ban-pot strong { font-size: 1.2rem; } - .user-search { - display: grid; - gap: 10px; - } - .user-results { - display: grid; - gap: 6px; - } - .user-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - padding: 6px 10px; - border-radius: 12px; - border: 1px solid var(--border); - background: var(--surface-3); - } - .user-row.is-selected { - border-color: var(--sea); - } - .user-main { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; - } - .user-name { - font-weight: 600; - } - .user-pills { - display: flex; - gap: 6px; - flex-wrap: wrap; - } - .user-pill { - font-size: 0.7rem; - padding: 2px 6px; - border-radius: 999px; - background: var(--surface-3); - color: var(--ink-soft); - } - .user-expand { - background: transparent; - border: none; - color: var(--ink-soft); - cursor: pointer; - font-size: 0.75rem; - } - .user-details { - display: none; - font-size: 0.78rem; - color: var(--ink-soft); - margin-left: 4px; - } - .user-row.is-open .user-details { - display: inline; - } - .user-select { - background: var(--sea); - color: white; - border: none; - border-radius: 999px; - padding: 4px 10px; - cursor: pointer; - font-size: 0.75rem; - }
@@ -206,10 +139,12 @@
-