Lumi/plugins/okf/views/admin.ejs
2026-06-18 23:01:27 +02:00

323 lines
14 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."
}) %>
<form method="get" action="/plugins/okf/admin" class="log-controls">
<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">Reset</a>
</form>
</section>
<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>
<a class="button subtle" href="/plugins/okf">Open OKF</a>
</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?edit=<%= 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>
<section class="card" id="okf-editor">
<div class="section-header">
<div>
<h2><%= selected ? "Edit OKF entry" : "Create OKF entry" %></h2>
<p class="hint">Markdown is sanitized before rendering. User, moderator, and admin content are filtered server-side.</p>
</div>
<% if (selected) { %>
<a class="button subtle" href="/plugins/okf/admin">Create new</a>
<% } %>
</div>
<form method="post" action="<%= selected ? `/plugins/okf/admin/entries/${selected.slug}` : '/plugins/okf/admin/entries' %>" class="form-grid">
<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" value="<%= selected ? selected.category : '' %>" placeholder="General, Support, Commands" />
</div>
<div class="field">
<label>Tags</label>
<input name="tags" value="<%= selected ? selected.tags.join(', ') : '' %>" placeholder="Comma-separated tags" />
</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"><%= selected ? selected.user_markdown : '' %></textarea>
</div>
<div class="field full">
<label>Moderator/support Markdown details</label>
<textarea name="moderator_markdown" rows="6"><%= selected ? selected.moderator_markdown : '' %></textarea>
</div>
<div class="field full">
<label>Admin/internal Markdown details</label>
<textarea name="admin_markdown" rows="6"><%= selected ? selected.admin_markdown : '' %></textarea>
</div>
<div class="field full">
<label>AI-facing facts/context</label>
<textarea name="ai_facts_markdown" rows="6"><%= 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"><%= selected ? "Save OKF entry" : "Create OKF entry" %></button>
</div>
</form>
<% if (selected) { %>
<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>
<% } %>
</section>
<% if (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>
<% } %>
<%- include("../../../src/web/views/partials/layout-bottom") %>