Lumi/plugins/okf/views/admin.ejs
2026-06-25 14:10:04 +02:00

733 lines
36 KiB
Plaintext

<%- include("../../../src/web/views/partials/layout-top", { title }) %>
<section class="card">
<%- include("../../../src/web/views/partials/page-header", {
eyebrow: "Administration",
pageTitle: "OKF Management",
description: "Manage role-gated knowledge entries, review state, version history, and OKF-specific editing permissions."
}) %>
<nav class="tabs" aria-label="OKF management sections">
<a href="/plugins/okf/admin?tab=general" <%= activeTab === "general" ? 'aria-current="page"' : "" %>>General OKF</a>
<a href="/plugins/okf/admin?tab=community" <%= activeTab === "community" ? 'aria-current="page"' : "" %>>Community OKF</a>
<a href="/plugins/okf/admin?tab=system" <%= activeTab === "system" ? 'aria-current="page"' : "" %>>System-generated OKF</a>
</nav>
<% if (activeTab === "general") { %>
<form method="get" action="/plugins/okf/admin" class="log-controls">
<input type="hidden" name="tab" value="general" />
<label>
<span>Search</span>
<input name="q" value="<%= filters.q %>" placeholder="Search visible OKF fields" />
</label>
<label>
<span>Status</span>
<select name="status">
<option value="">All statuses</option>
<% statuses.forEach((status) => { %>
<option value="<%= status %>" <%= filters.status === status ? "selected" : "" %>><%= status %></option>
<% }) %>
</select>
</label>
<label>
<span>Category</span>
<select name="category">
<option value="">All categories</option>
<% categories.forEach((category) => { %>
<option value="<%= category %>" <%= filters.category === category ? "selected" : "" %>><%= category %></option>
<% }) %>
</select>
</label>
<label>
<span>Tag</span>
<select name="tag">
<option value="">All tags</option>
<% tags.forEach((tag) => { %>
<option value="<%= tag %>" <%= filters.tag === tag ? "selected" : "" %>><%= tag %></option>
<% }) %>
</select>
</label>
<button class="button subtle" type="submit">Filter</button>
<a class="button subtle" href="/plugins/okf/admin?tab=general">Reset</a>
</form>
<% } else if (activeTab === "community") { %>
<p class="hint">Community files are locally maintained OKF Markdown under <code>knowledge/community</code>.</p>
<% } else { %>
<p class="hint">System-generated files are read-only OKF Markdown from core and plugin metadata.</p>
<% } %>
<datalist id="okf-category-suggestions">
<% categories.forEach((category) => { %>
<option value="<%= category %>"></option>
<% }) %>
</datalist>
<datalist id="okf-tag-suggestions">
<% tags.forEach((tag) => { %>
<option value="<%= tag %>"></option>
<% }) %>
</datalist>
</section>
<% if (activeTab === "general") { %>
<section class="card">
<div class="section-header">
<div>
<h2>Entries</h2>
<p class="hint"><%= entries.length %> entr<%= entries.length === 1 ? "y" : "ies" %> shown.</p>
</div>
<div class="button-group">
<button class="button" type="button" data-okf-create-open>Create OKF entry</button>
<a class="button subtle" href="/plugins/okf">Open OKF</a>
</div>
</div>
<% if (!entries.length) { %>
<div class="empty-state">No OKF entries match this filter.</div>
<% } else { %>
<div class="table-wrap">
<table class="table">
<thead>
<tr>
<th>Entry</th>
<th>Status</th>
<th>Visibility</th>
<th>Review</th>
<th>Updated</th>
</tr>
</thead>
<tbody>
<% entries.forEach((entry) => { %>
<tr>
<td>
<a href="/plugins/okf/admin?tab=general&edit=<%= encodeURIComponent(entry.slug) %>"><strong><%= entry.title %></strong></a>
<p class="hint"><%= entry.slug %> · <%= entry.category || "General" %></p>
</td>
<td><span class="badge"><%= entry.status %></span></td>
<td><%= entry.visibility %></td>
<td><%= entry.review_state %></td>
<td><%= new Date(entry.updated_at).toLocaleString() %></td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<% } %>
</section>
<% } %>
<% if (activeTab === "community") { %>
<section class="card" id="okf-community-files">
<div class="section-header">
<div>
<h2>Community OKF files</h2>
<p class="hint">Edit community-specific Markdown knowledge stored under <code>knowledge/community</code>. Generated core and plugin files stay separate.</p>
</div>
</div>
<div data-okf-file-list>
<form class="log-controls" data-okf-file-filters>
<label><span>Search files</span><input type="search" data-okf-file-search placeholder="Title, ID, category, tag" /></label>
<label><span>Category</span><select data-okf-file-filter="category"><option value="">All categories</option><% [...new Set(communityFiles.map((file) => file.category).filter(Boolean))].sort().forEach((value) => { %><option value="<%= value %>"><%= value %></option><% }) %></select></label>
<label><span>Status</span><select data-okf-file-filter="status"><option value="">All statuses</option><% [...new Set(communityFiles.map((file) => file.status).filter(Boolean))].sort().forEach((value) => { %><option value="<%= value %>"><%= value %></option><% }) %></select></label>
<label><span>Visibility</span><select data-okf-file-filter="visibility"><option value="">All visibility</option><% [...new Set(communityFiles.map((file) => file.visibility).filter(Boolean))].sort().forEach((value) => { %><option value="<%= value %>"><%= value %></option><% }) %></select></label>
<button class="button subtle" type="reset">Reset</button>
</form>
<p class="hint" data-okf-file-count><%= communityFiles.length %> file<%= communityFiles.length === 1 ? "" : "s" %> shown.</p>
<% if (!communityFiles.length) { %>
<div class="empty-state">No community OKF files yet.</div>
<% } else { %>
<div class="table-wrap">
<table class="table">
<thead><tr><th>File</th><th>Category</th><th>Status</th><th>Visibility</th><th>Updated</th></tr></thead>
<tbody>
<% communityFiles.forEach((file) => { %>
<tr data-okf-file-row data-category="<%= file.category %>" data-status="<%= file.status %>" data-visibility="<%= file.visibility %>" data-search="<%= [file.title, file.id, file.slug, file.category, ...(file.tags || [])].filter(Boolean).join(' ').toLowerCase() %>">
<td><a href="/plugins/okf/admin?tab=community&community=<%= encodeURIComponent(file.slug) %>#okf-community-files"><strong><%= file.title %></strong></a><p class="hint"><%= file.id %> · <%= file.slug %>.md</p></td>
<td><%= file.category || "Community" %></td>
<td><span class="badge"><%= file.status %></span></td>
<td><%= file.visibility %></td>
<td><%= file.updated_at ? new Date(file.updated_at).toLocaleString() : "Unknown" %></td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<div class="empty-state" data-okf-file-empty hidden>No community OKF files match these filters.</div>
<% } %>
</div>
<article class="stat-card">
<span class="stat-label"><%= selectedCommunity ? "Edit file" : "Create file" %></span>
<form method="post" action="<%= selectedCommunity ? `/plugins/okf/admin/community/${encodeURIComponent(selectedCommunity.slug)}` : '/plugins/okf/admin/community' %>" class="form-grid">
<div class="field">
<label>Title</label>
<input name="title" required value="<%= selectedCommunity ? selectedCommunity.title : '' %>" placeholder="Community currency" />
</div>
<div class="field">
<label>Slug</label>
<input name="slug" value="<%= selectedCommunity ? selectedCommunity.slug : '' %>" placeholder="community-currency" />
<span class="hint">Used as the Markdown filename.</span>
</div>
<div class="field">
<label>ID</label>
<input name="id" value="<%= selectedCommunity ? selectedCommunity.id : '' %>" placeholder="community.currency" />
</div>
<div class="field">
<label>Category</label>
<input class="okf-text-suggestion" name="category" list="okf-category-suggestions" value="<%= selectedCommunity ? selectedCommunity.category : 'Community' %>" />
<span class="hint">Existing categories appear as suggestions.</span>
</div>
<div class="field">
<label>Status</label>
<select name="status">
<% ["active", "published", "draft", "archived", "disabled"].forEach((status) => { %>
<option value="<%= status %>" <%= selectedCommunity && selectedCommunity.status === status ? "selected" : "" %>><%= status %></option>
<% }) %>
</select>
</div>
<div class="field">
<label>Visibility</label>
<select name="visibility">
<% visibilityValues.forEach((value) => { %>
<option value="<%= value %>" <%= selectedCommunity && selectedCommunity.visibility === value ? "selected" : "" %>><%= value %></option>
<% }) %>
</select>
</div>
<div class="field">
<label>Priority</label>
<input name="priority" type="number" step="1" value="<%= selectedCommunity ? selectedCommunity.priority : 0 %>" />
</div>
<div class="field">
<label>Tags</label>
<input name="tags" list="okf-tag-suggestions" value="<%= selectedCommunity ? selectedCommunity.tags.join(', ') : '' %>" placeholder="currency, roles, rules" />
<span class="hint">Use comma-separated tags. Existing tags appear as suggestions while typing.</span>
</div>
<div class="field full">
<label>Markdown body</label>
<textarea name="body" rows="14" placeholder="# Community facts" data-placeholder-field="okf.markdown" data-placeholder-output-audience="user"><%= selectedCommunity ? selectedCommunity.body : '' %></textarea>
<span class="hint">You can reference visible frontmatter values with placeholders such as <code>{{community.currency.primary_name}}</code>.</span>
</div>
<% if (selectedCommunity && (selectedCommunity.generated || !selectedCommunity.editable)) { %>
<div class="field full">
<p class="notice error">This file is marked generated or non-editable and cannot be saved from this editor.</p>
</div>
<% } %>
<div class="field full button-group centered">
<button class="button" type="submit" <%= selectedCommunity && (selectedCommunity.generated || !selectedCommunity.editable) ? "disabled" : "" %>><%= selectedCommunity ? "Save community file" : "Create community file" %></button>
<% if (selectedCommunity) { %>
<a class="button subtle" href="/plugins/okf/admin?tab=community#okf-community-files">Create new file</a>
<% } %>
</div>
</form>
</article>
<% if (selectedCommunity) { %>
<details class="feedback-metadata" open>
<summary>Preview</summary>
<p class="hint"><%= selectedCommunity.path %> · <%= selectedCommunity.id %></p>
<div class="feedback-copy-block"><%- renderMarkdown(selectedCommunity.body || "_No Markdown body yet._") %></div>
</details>
<% } %>
</section>
<% } %>
<% if (activeTab === "system") { %>
<section class="card" id="okf-system-files">
<div class="section-header">
<div>
<h2>System-generated OKF</h2>
<p class="hint">Read-only generated knowledge used by AI support and scoped by user permission level.</p>
</div>
</div>
<div data-okf-file-list>
<form class="log-controls" data-okf-file-filters>
<label><span>Search files</span><input type="search" data-okf-file-search placeholder="Title, ID, scope, category, tag" /></label>
<label><span>Category</span><select data-okf-file-filter="category"><option value="">All categories</option><% [...new Set(systemFiles.map((file) => file.category).filter(Boolean))].sort().forEach((value) => { %><option value="<%= value %>"><%= value %></option><% }) %></select></label>
<label><span>Scope</span><select data-okf-file-filter="scope"><option value="">All scopes</option><% [...new Set(systemFiles.map((file) => file.scope).filter(Boolean))].sort().forEach((value) => { %><option value="<%= value %>"><%= value %></option><% }) %></select></label>
<label><span>Visibility</span><select data-okf-file-filter="visibility"><option value="">All visibility</option><% [...new Set(systemFiles.map((file) => file.visibility).filter(Boolean))].sort().forEach((value) => { %><option value="<%= value %>"><%= value %></option><% }) %></select></label>
<button class="button subtle" type="reset">Reset</button>
</form>
<p class="hint" data-okf-file-count><%= systemFiles.length %> file<%= systemFiles.length === 1 ? "" : "s" %> shown. Generated files are rebuilt from repository metadata and are read-only here.</p>
<% if (!systemFiles.length) { %>
<div class="empty-state">No generated OKF files were found.</div>
<% } else { %>
<div class="table-wrap">
<table class="table">
<thead><tr><th>File</th><th>Scope</th><th>Category</th><th>Status</th><th>Visibility</th></tr></thead>
<tbody>
<% systemFiles.forEach((file) => { %>
<tr data-okf-file-row data-category="<%= file.category %>" data-scope="<%= file.scope %>" data-visibility="<%= file.visibility %>" data-search="<%= [file.title, file.id, file.slug, file.scope, file.category, ...(file.tags || [])].filter(Boolean).join(' ').toLowerCase() %>">
<td><a href="/plugins/okf/admin?tab=system&system=<%= encodeURIComponent(file.slug) %>#okf-system-files"><strong><%= file.title %></strong></a><p class="hint"><%= file.id %> · <%= file.path %></p></td>
<td><%= file.scope %></td>
<td><%= file.category || "General" %></td>
<td><span class="badge"><%= file.status %></span></td>
<td><%= file.visibility %></td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<div class="empty-state" data-okf-file-empty hidden>No system-generated OKF files match these filters.</div>
<% } %>
</div>
<div class="callout"><strong>Role-scoped support context</strong><p>User, moderator, and admin visibility still applies during retrieval. Casual requests receive support-level facts; development-focused admin requests may use deeper technical details.</p></div>
<% if (selectedSystemFile) { %>
<details class="feedback-metadata" open>
<summary><%= selectedSystemFile.title %></summary>
<p class="hint">
<%= selectedSystemFile.path %> · <%= selectedSystemFile.id %> · <%= selectedSystemFile.status %> · <%= selectedSystemFile.visibility %>
</p>
<div class="feedback-copy-block"><%- renderMarkdown(selectedSystemFile.body || "_No Markdown body available._") %></div>
</details>
<% } else if (systemFiles.length) { %>
<div class="empty-state">Select a generated OKF file to preview its current Markdown.</div>
<% } %>
</section>
<% } %>
<% if (selected) { %>
<div class="modal-backdrop okf-edit-modal is-open" id="okf-editor" data-okf-edit-modal aria-hidden="false">
<div class="modal okf-entry-modal-dialog okf-entry-fullscreen-modal" role="dialog" aria-modal="true" aria-labelledby="okf-edit-title">
<div class="modal-header">
<div>
<h2 id="okf-edit-title">Edit OKF entry</h2>
<p class="hint">Markdown is sanitized before rendering. User, moderator, and admin content are filtered server-side.</p>
</div>
<div class="button-group">
<a class="button subtle" href="/plugins/okf/admin?tab=general">Create new</a>
<button class="button subtle" type="button" data-okf-edit-close>Close</button>
</div>
</div>
<form method="post" action="/plugins/okf/admin/entries/<%= selected.slug %>" class="form-grid" data-okf-edit-form data-okf-draft-key="lumi.okf.edit.<%= selected.slug %>">
<div class="field">
<label>Title</label>
<input name="title" required value="<%= selected ? selected.title : '' %>" />
</div>
<div class="field">
<label>Slug</label>
<input name="slug" required value="<%= selected ? selected.slug : '' %>" />
</div>
<div class="field">
<label>Category</label>
<input name="category" list="okf-category-suggestions" value="<%= selected ? selected.category : '' %>" placeholder="General, Support, Commands" />
</div>
<div class="field">
<label>Tags</label>
<input name="tags" list="okf-tag-suggestions" value="<%= selected ? selected.tags.join(', ') : '' %>" placeholder="Comma-separated tags" />
<span class="hint">Use comma-separated tags. Existing tags appear as suggestions while typing.</span>
</div>
<div class="field">
<label>Visibility</label>
<select name="visibility">
<% visibilityValues.forEach((value) => { %>
<option value="<%= value %>" <%= selected && selected.visibility === value ? "selected" : "" %>><%= value %></option>
<% }) %>
</select>
</div>
<div class="field">
<label>Status</label>
<select name="status" <%= okfAccess.canImplement ? "" : "disabled" %>>
<% statuses.forEach((status) => { %>
<option value="<%= status %>" <%= selected && selected.status === status ? "selected" : "" %>><%= status %></option>
<% }) %>
</select>
<% if (!okfAccess.canImplement) { %><span class="hint">Editors can propose changes; publishing requires implement permission.</span><% } %>
</div>
<div class="field">
<label>Review state</label>
<select name="review_state" <%= okfAccess.canImplement ? "" : "disabled" %>>
<% reviewStates.forEach((state) => { %>
<option value="<%= state %>" <%= selected && selected.review_state === state ? "selected" : "" %>><%= state %></option>
<% }) %>
</select>
</div>
<div class="field">
<label>Aliases / related questions</label>
<textarea name="aliases" rows="3" placeholder="One related question per line"><%= selected ? selected.aliases.join('\n') : '' %></textarea>
<span class="hint">Use one question per line. Commas are kept as part of the question.</span>
</div>
<div class="field full">
<label>Short summary</label>
<textarea name="summary" rows="3"><%= selected ? selected.summary : '' %></textarea>
</div>
<div class="field full">
<label>User-facing Markdown answer</label>
<textarea name="user_markdown" rows="8" data-placeholder-field="okf.markdown" data-placeholder-output-audience="user"><%= selected ? selected.user_markdown : '' %></textarea>
</div>
<div class="field full">
<label>Moderator/support Markdown details</label>
<textarea name="moderator_markdown" rows="6" data-placeholder-field="okf.markdown" data-placeholder-output-audience="mod"><%= selected ? selected.moderator_markdown : '' %></textarea>
</div>
<div class="field full">
<label>Admin/internal Markdown details</label>
<textarea name="admin_markdown" rows="6" data-placeholder-field="okf.markdown" data-placeholder-output-audience="admin"><%= selected ? selected.admin_markdown : '' %></textarea>
</div>
<div class="field full">
<label>AI-facing facts/context</label>
<textarea name="ai_facts_markdown" rows="6" data-placeholder-field="okf.markdown" data-placeholder-output-audience="admin"><%= selected ? selected.ai_facts_markdown : '' %></textarea>
<span class="hint">Stored now for future AI retrieval integration. The public UI only shows this to admins/editors.</span>
</div>
<div class="field full">
<label>Source links / references</label>
<textarea name="source_links" rows="3" placeholder="One URL or local path per line"><%= selected ? selected.source_links.join('\n') : '' %></textarea>
</div>
<div class="field full">
<label>Change note</label>
<input name="change_note" placeholder="Optional note for version history" />
</div>
<div class="field full button-group centered">
<button class="button" type="submit">Save OKF entry</button>
<button class="button subtle" type="button" data-okf-edit-reset>Reset draft</button>
<button class="button subtle" type="button" data-okf-edit-close>Close</button>
</div>
</form>
<details class="feedback-metadata">
<summary>Role preview</summary>
<div class="stats-grid">
<article class="stat-card">
<span class="stat-label">User view</span>
<strong><%= selected.title %></strong>
<p><%= selected.summary || "No summary provided." %></p>
<div class="feedback-copy-block"><%- renderMarkdown(selected.user_markdown || "_No user-facing answer yet._") %></div>
</article>
<article class="stat-card">
<span class="stat-label">Moderator view</span>
<strong><%= selected.title %></strong>
<p><%= selected.summary || "No summary provided." %></p>
<div class="feedback-copy-block"><%- renderMarkdown(selected.user_markdown || "_No user-facing answer yet._") %></div>
<div class="feedback-copy-block"><%- renderMarkdown(selected.moderator_markdown || "_No moderator/support details yet._") %></div>
</article>
<article class="stat-card">
<span class="stat-label">Admin/editor view</span>
<strong><%= selected.title %></strong>
<p><%= selected.summary || "No summary provided." %></p>
<div class="feedback-copy-block"><%- renderMarkdown(selected.user_markdown || "_No user-facing answer yet._") %></div>
<div class="feedback-copy-block"><%- renderMarkdown(selected.moderator_markdown || "_No moderator/support details yet._") %></div>
<div class="feedback-copy-block"><%- renderMarkdown(selected.admin_markdown || "_No admin/internal details yet._") %></div>
</article>
</div>
<p class="hint">AI facts are stored separately for future role-aware retrieval and are not shown in normal user/mod previews.</p>
</details>
<div class="button-group centered">
<% if (okfAccess.canReview) { %>
<form method="post" action="/plugins/okf/admin/entries/<%= selected.slug %>/review" class="inline-form">
<button class="button subtle" type="submit">Mark reviewed</button>
</form>
<% } %>
<% if (okfAccess.canImplement) { %>
<form method="post" action="/plugins/okf/admin/entries/<%= selected.slug %>/publish" class="inline-form">
<button class="button subtle" type="submit">Publish</button>
</form>
<form method="post" action="/plugins/okf/admin/entries/<%= selected.slug %>/archive" class="inline-form">
<button class="button subtle" type="submit">Archive</button>
</form>
<form method="post" action="/plugins/okf/admin/entries/<%= selected.slug %>/restore" class="inline-form">
<button class="button subtle" type="submit">Restore as draft</button>
</form>
<form method="post" action="/plugins/okf/admin/entries/<%= selected.slug %>/delete" class="inline-form" data-confirm-mode="modal" data-confirm-text="Delete this OKF entry? The version history is kept for audit, but the entry will no longer be visible.">
<button class="button danger" type="submit">Delete</button>
</form>
<% } %>
</div>
<details class="feedback-metadata">
<summary>Version history</summary>
<% if (!versions.length) { %>
<p class="hint">No versions recorded yet.</p>
<% } else { %>
<% versions.forEach((version) => { %>
<article class="feedback-copy-block">
<strong>Version <%= version.version_number %> · <%= version.change_type %></strong>
<p class="hint"><%= new Date(version.created_at).toLocaleString() %> · <%= version.changed_by || "system" %> · <%= version.note || "No note." %></p>
<details>
<summary>Snapshot</summary>
<pre><%= JSON.stringify(version.next, null, 2) %></pre>
</details>
<% if (okfAccess.canImplement) { %>
<form method="post" action="/plugins/okf/admin/entries/<%= selected.slug %>/versions/<%= version.version_number %>/restore" class="inline-form" data-confirm-mode="modal" data-confirm-text="Restore this OKF entry to version <%= version.version_number %>? A new version will be recorded for the restore.">
<input type="hidden" name="note" value="Restored from version <%= version.version_number %>." />
<button class="button subtle" type="submit">Restore this version</button>
</form>
<% } %>
</article>
<% }) %>
<% } %>
</details>
</div>
</div>
<% } %>
<div class="modal-backdrop okf-create-modal" data-okf-create-modal aria-hidden="true">
<div class="modal okf-entry-modal-dialog" role="dialog" aria-modal="true" aria-labelledby="okf-create-title">
<div class="modal-header">
<div>
<h2 id="okf-create-title">Create OKF entry</h2>
<p class="hint">Markdown is sanitized before rendering. User, moderator, and admin content are filtered server-side.</p>
</div>
<button class="button subtle" type="button" data-okf-create-close>Close</button>
</div>
<form method="post" action="/plugins/okf/admin/entries" class="form-grid">
<div class="field">
<label>Title</label>
<input name="title" required />
</div>
<div class="field">
<label>Slug</label>
<input name="slug" required />
</div>
<div class="field">
<label>Category</label>
<input name="category" list="okf-category-suggestions" placeholder="General, Support, Commands" />
</div>
<div class="field">
<label>Tags</label>
<input name="tags" list="okf-tag-suggestions" placeholder="Comma-separated tags" />
<span class="hint">Use comma-separated tags. Existing tags appear as suggestions while typing.</span>
</div>
<div class="field">
<label>Visibility</label>
<select name="visibility">
<% visibilityValues.forEach((value) => { %>
<option value="<%= value %>"><%= value %></option>
<% }) %>
</select>
</div>
<div class="field">
<label>Status</label>
<select name="status" <%= okfAccess.canImplement ? "" : "disabled" %>>
<% statuses.forEach((status) => { %>
<option value="<%= status %>"><%= status %></option>
<% }) %>
</select>
<% if (!okfAccess.canImplement) { %><span class="hint">Editors can propose changes; publishing requires implement permission.</span><% } %>
</div>
<div class="field">
<label>Review state</label>
<select name="review_state" <%= okfAccess.canImplement ? "" : "disabled" %>>
<% reviewStates.forEach((state) => { %>
<option value="<%= state %>"><%= state %></option>
<% }) %>
</select>
</div>
<div class="field">
<label>Aliases / related questions</label>
<textarea name="aliases" rows="3" placeholder="One related question per line"></textarea>
<span class="hint">Use one question per line. Commas are kept as part of the question.</span>
</div>
<div class="field full">
<label>Short summary</label>
<textarea name="summary" rows="3"></textarea>
</div>
<div class="field full">
<label>User-facing Markdown answer</label>
<textarea name="user_markdown" rows="8" data-placeholder-field="okf.markdown" data-placeholder-output-audience="user"></textarea>
</div>
<div class="field full">
<label>Moderator/support Markdown details</label>
<textarea name="moderator_markdown" rows="6" data-placeholder-field="okf.markdown" data-placeholder-output-audience="mod"></textarea>
</div>
<div class="field full">
<label>Admin/internal Markdown details</label>
<textarea name="admin_markdown" rows="6" data-placeholder-field="okf.markdown" data-placeholder-output-audience="admin"></textarea>
</div>
<div class="field full">
<label>AI-facing facts/context</label>
<textarea name="ai_facts_markdown" rows="6" data-placeholder-field="okf.markdown" data-placeholder-output-audience="admin"></textarea>
<span class="hint">Stored now for future AI retrieval integration. The public UI only shows this to admins/editors.</span>
</div>
<div class="field full">
<label>Source links / references</label>
<textarea name="source_links" rows="3" placeholder="One URL or local path per line"></textarea>
</div>
<div class="field full">
<label>Change note</label>
<input name="change_note" placeholder="Optional note for version history" />
</div>
<div class="field full button-group centered">
<button class="button" type="submit">Create OKF entry</button>
<button class="button subtle" type="button" data-okf-create-close>Cancel</button>
</div>
</form>
</div>
</div>
<% if (activeTab === "general" && okfAccess.canManagePermissions) { %>
<section class="card" id="okf-permissions">
<div class="section-header">
<div>
<h2>OKF permission grants</h2>
<p class="hint">These grants are independent from normal Lumi admin/mod roles.</p>
</div>
</div>
<form method="post" action="/plugins/okf/admin/permissions" class="form-grid">
<div class="field lumi-user-lookup" data-user-lookup>
<label>User</label>
<input type="search" autocomplete="off" placeholder="Search name, username, platform ID, or user ID" data-user-lookup-search />
<input type="hidden" name="user_id" required data-user-lookup-id />
<div class="lumi-user-lookup-results" data-user-lookup-results hidden></div>
<div class="lumi-user-lookup-preview" data-user-lookup-preview hidden></div>
</div>
<div class="field">
<label>Permission</label>
<select name="level" required>
<% levels.forEach((level) => { %>
<option value="<%= level %>"><%= level %></option>
<% }) %>
</select>
</div>
<div class="field full">
<label>Notes</label>
<input name="notes" placeholder="Optional reason for this grant" />
</div>
<div class="field full button-group centered">
<button class="button" type="submit">Grant OKF permission</button>
</div>
</form>
<% if (!permissions.length) { %>
<p class="hint">No OKF-specific permissions have been granted.</p>
<% } else { %>
<div class="table-wrap">
<table class="table">
<thead>
<tr>
<th>User</th>
<th>Level</th>
<th>Granted</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<% permissions.forEach((grant) => { %>
<tr>
<td><%= grant.user_name || grant.user_id %></td>
<td><%= grant.level %></td>
<td><%= new Date(grant.created_at).toLocaleString() %><br><span class="hint">by <%= grant.granted_by_name || grant.granted_by || "unknown" %></span></td>
<td><%= grant.revoked_at ? `Revoked ${new Date(grant.revoked_at).toLocaleString()}` : "Active" %></td>
<td>
<% if (!grant.revoked_at) { %>
<form method="post" action="/plugins/okf/admin/permissions/<%= grant.id %>/revoke" class="inline-form">
<button class="button subtle" type="submit">Revoke</button>
</form>
<% } %>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<% } %>
</section>
<% } %>
<script>
document.querySelectorAll("[data-okf-file-list]").forEach((browser) => {
const form = browser.querySelector("[data-okf-file-filters]");
const search = browser.querySelector("[data-okf-file-search]");
const rows = [...browser.querySelectorAll("[data-okf-file-row]")];
const count = browser.querySelector("[data-okf-file-count]");
const empty = browser.querySelector("[data-okf-file-empty]");
const applyFilters = () => {
const query = String(search?.value || "").trim().toLowerCase();
const filters = [...browser.querySelectorAll("[data-okf-file-filter]")];
let visible = 0;
rows.forEach((row) => {
const matchesSearch = !query || String(row.dataset.search || "").includes(query);
const matchesFilters = filters.every((field) => !field.value || row.dataset[field.dataset.okfFileFilter] === field.value);
row.hidden = !(matchesSearch && matchesFilters);
if (!row.hidden) visible += 1;
});
if (count) count.textContent = `${visible} file${visible === 1 ? "" : "s"} shown.`;
if (empty) empty.hidden = visible !== 0;
};
form?.addEventListener("input", applyFilters);
form?.addEventListener("change", applyFilters);
form?.addEventListener("submit", (event) => event.preventDefault());
form?.addEventListener("reset", () => window.setTimeout(applyFilters, 0));
});
(() => {
const modal = document.querySelector("[data-okf-create-modal]");
const openButtons = document.querySelectorAll("[data-okf-create-open]");
const closeButtons = modal?.querySelectorAll("[data-okf-create-close]") || [];
const firstInput = modal?.querySelector("input[name='title']");
const openModal = () => {
if (!modal) return;
modal.classList.add("is-open");
modal.setAttribute("aria-hidden", "false");
window.setTimeout(() => firstInput?.focus(), 0);
};
const closeModal = () => {
if (!modal) return;
modal.classList.remove("is-open");
modal.setAttribute("aria-hidden", "true");
};
openButtons.forEach((button) => button.addEventListener("click", openModal));
closeButtons.forEach((button) => button.addEventListener("click", closeModal));
modal?.addEventListener("click", (event) => {
if (event.target === modal) closeModal();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && modal?.classList.contains("is-open")) {
closeModal();
}
});
})();
(() => {
const modal = document.querySelector("[data-okf-edit-modal]");
const form = document.querySelector("[data-okf-edit-form]");
if (!modal || !form) return;
const storageKey = form.dataset.okfDraftKey || "lumi.okf.edit";
const fieldSelector = "input[name], textarea[name], select[name]";
const firstInput = form.querySelector("input[name='title']");
const draftFields = () => Array.from(form.querySelectorAll(fieldSelector))
.filter((field) => field.name && field.type !== "hidden" && !field.disabled);
const saveDraft = () => {
const draft = {};
draftFields().forEach((field) => {
draft[field.name] = field.value || "";
});
try {
window.sessionStorage?.setItem(storageKey, JSON.stringify(draft));
} catch {}
};
const loadDraft = () => {
try {
return JSON.parse(window.sessionStorage?.getItem(storageKey) || "null") || null;
} catch {
return null;
}
};
const clearDraft = () => {
try {
window.sessionStorage?.removeItem(storageKey);
} catch {}
};
const restoreDraft = () => {
const draft = loadDraft();
if (!draft) return;
draftFields().forEach((field) => {
if (Object.prototype.hasOwnProperty.call(draft, field.name)) {
field.value = draft[field.name];
}
});
};
const closeModal = () => {
saveDraft();
modal.classList.remove("is-open");
modal.setAttribute("aria-hidden", "true");
};
const openModal = () => {
modal.classList.add("is-open");
modal.setAttribute("aria-hidden", "false");
window.setTimeout(() => firstInput?.focus(), 0);
};
restoreDraft();
openModal();
form.addEventListener("input", saveDraft);
form.addEventListener("change", saveDraft);
form.addEventListener("submit", clearDraft);
modal.querySelectorAll("[data-okf-edit-close]").forEach((button) => {
button.addEventListener("click", closeModal);
});
modal.querySelector("[data-okf-edit-reset]")?.addEventListener("click", () => {
clearDraft();
window.location.reload();
});
})();
</script>
<%- include("../../../src/web/views/partials/layout-bottom") %>