275 lines
13 KiB
Plaintext
275 lines
13 KiB
Plaintext
<%- include("partials/layout-top", { title }) %>
|
|
<section class="card">
|
|
<%- include("partials/page-header", {
|
|
eyebrow: "Administration",
|
|
pageTitle: "Feedback review",
|
|
description: "Review core feedback, reply to submitters, record private work notes, and close or delete resolved reports."
|
|
}) %>
|
|
<form method="get" action="/admin/feedback" class="log-controls feedback-admin-filters">
|
|
<label>
|
|
<span>Status</span>
|
|
<select name="status">
|
|
<option value="">Active statuses</option>
|
|
<% feedbackOptions.statuses.forEach((status) => { %>
|
|
<option value="<%= status.value %>" <%= filters.status === status.value ? "selected" : "" %>><%= status.label %></option>
|
|
<% }) %>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span>Category</span>
|
|
<select name="category">
|
|
<option value="">All categories</option>
|
|
<% feedbackOptions.categories.forEach((category) => { %>
|
|
<option value="<%= category.value %>" <%= filters.category === category.value ? "selected" : "" %>><%= category.label %></option>
|
|
<% }) %>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span>Severity</span>
|
|
<select name="severity">
|
|
<option value="">All severities</option>
|
|
<% feedbackOptions.severities.forEach((severity) => { %>
|
|
<option value="<%= severity.value %>" <%= filters.severity === severity.value ? "selected" : "" %>><%= severity.label %></option>
|
|
<% }) %>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span>Scope</span>
|
|
<select name="scope">
|
|
<option value="">All scopes</option>
|
|
<% feedbackOptions.scopes.forEach((scope) => { %>
|
|
<option value="<%= scope.value %>" <%= filters.scope === scope.value ? "selected" : "" %>><%= scope.label %></option>
|
|
<% }) %>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span>Plugin/area</span>
|
|
<input name="area" value="<%= filters.area %>" placeholder="URL, plugin, page, area" />
|
|
</label>
|
|
<label>
|
|
<span>Submitter</span>
|
|
<input name="submitter" value="<%= filters.submitter %>" placeholder="Name or user ID" />
|
|
</label>
|
|
<label>
|
|
<span>From</span>
|
|
<input type="date" name="date_from" value="<%= filters.date_from %>" />
|
|
</label>
|
|
<label>
|
|
<span>To</span>
|
|
<input type="date" name="date_to" value="<%= filters.date_to %>" />
|
|
</label>
|
|
<label>
|
|
<span>Sort</span>
|
|
<select name="sort">
|
|
<% [
|
|
["last_activity", "Last activity"],
|
|
["newest", "Newest"],
|
|
["oldest", "Oldest"],
|
|
["severity", "Severity"],
|
|
["status", "Status"]
|
|
].forEach(([value, label]) => { %>
|
|
<option value="<%= value %>" <%= filters.sort === value ? "selected" : "" %>><%= label %></option>
|
|
<% }) %>
|
|
</select>
|
|
</label>
|
|
<label class="checkbox-inline">
|
|
<input type="checkbox" name="needs_action" value="1" <%= filters.needs_action === "1" ? "checked" : "" %> />
|
|
<span>Needs admin action</span>
|
|
</label>
|
|
<button class="button subtle" type="submit">Filter</button>
|
|
<a class="button subtle" href="/admin/feedback">Reset</a>
|
|
</form>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<div class="section-header">
|
|
<div>
|
|
<h2>Feedback queue</h2>
|
|
<p class="hint"><%= feedbackItems.length %> item(s) shown.</p>
|
|
</div>
|
|
</div>
|
|
<% if (!feedbackItems.length) { %>
|
|
<div class="empty-state">No feedback matches this filter.</div>
|
|
<% } else { %>
|
|
<div class="feedback-admin-list">
|
|
<% feedbackItems.forEach((item) => { %>
|
|
<details class="feedback-admin-item">
|
|
<summary>
|
|
<span>
|
|
<strong><%= item.summary %></strong>
|
|
<small><%= item.submitter_name || item.submitter_id %> · <%= item.category_label %> · <%= item.scope_label_display %> · <%= item.support_count || 0 %> also affected</small>
|
|
</span>
|
|
<span class="status-indicator feedback-status-<%= item.status %>"><%= item.status_label %></span>
|
|
</summary>
|
|
<div class="feedback-admin-body">
|
|
<div class="feedback-detail-grid">
|
|
<div><span>Submitter</span><strong><%= item.submitter_name || item.submitter_id %></strong></div>
|
|
<div><span>Severity</span><strong><%= item.severity_label %></strong></div>
|
|
<div><span>Also affected</span><strong><%= item.support_count || 0 %></strong></div>
|
|
<div><span>Created</span><strong><%= new Date(item.created_at).toLocaleString() %></strong></div>
|
|
<div><span>Last activity</span><strong><%= new Date(item.last_activity_at).toLocaleString() %></strong></div>
|
|
</div>
|
|
|
|
<div class="feedback-copy-block">
|
|
<strong>Description</strong>
|
|
<p><%= item.description %></p>
|
|
</div>
|
|
<% if (item.steps_to_reproduce) { %><div class="feedback-copy-block"><strong>Steps</strong><p><%= item.steps_to_reproduce %></p></div><% } %>
|
|
<div class="feedback-two-col">
|
|
<div class="feedback-copy-block"><strong>Expected</strong><p><%= item.expected_behavior || "-" %></p></div>
|
|
<div class="feedback-copy-block"><strong>Actual</strong><p><%= item.actual_behavior || "-" %></p></div>
|
|
</div>
|
|
|
|
<details class="feedback-metadata">
|
|
<summary>Target and diagnostics</summary>
|
|
<div class="feedback-two-col">
|
|
<pre><%= JSON.stringify(item.target_metadata || {}, null, 2) %></pre>
|
|
<pre><%= JSON.stringify(item.diagnostics || {}, null, 2) %></pre>
|
|
</div>
|
|
<% if (item.current_url) { %><p><strong>URL:</strong> <a href="<%= item.current_url %>"><%= item.current_url %></a></p><% } %>
|
|
<% if (item.page_title) { %><p><strong>Page title:</strong> <%= item.page_title %></p><% } %>
|
|
<% if (item.screenshot) { %>
|
|
<p><strong>Screenshot:</strong> <a href="/feedback/<%= item.id %>/screenshot" target="_blank" rel="noopener">Open attached screenshot</a> <span class="hint"><%= Math.max(1, Math.round((item.screenshot.size || 0) / 1024)) %> KB</span></p>
|
|
<% } %>
|
|
<% if (item.attachments && item.attachments.length) { %>
|
|
<p><strong>Attachments:</strong></p>
|
|
<div class="feedback-attachment-list">
|
|
<% item.attachments.forEach((attachment) => { %>
|
|
<a class="button subtle" href="/feedback/<%= item.id %>/attachments/<%= attachment.id %>"><%= attachment.original_name %> (<%= Math.max(1, Math.round((attachment.size || 0) / 1024)) %> KB)</a>
|
|
<% }) %>
|
|
</div>
|
|
<% } %>
|
|
</details>
|
|
|
|
<details class="feedback-metadata">
|
|
<summary>Sensitive data cleanup</summary>
|
|
<form
|
|
method="post"
|
|
action="/admin/feedback/<%= item.id %>/cleanup"
|
|
class="form-grid"
|
|
data-confirm-mode="modal"
|
|
data-confirm-title="Clean feedback data"
|
|
data-confirm-text="This removes selected diagnostic or attachment data from this feedback item. The action is recorded as a private work note."
|
|
data-confirm-label="Clean data"
|
|
>
|
|
<label class="checkbox-inline">
|
|
<input type="checkbox" name="clear_screenshot" value="1" <%= item.screenshot ? "" : "disabled" %> />
|
|
<span>Remove screenshot</span>
|
|
</label>
|
|
<label class="checkbox-inline">
|
|
<input type="checkbox" name="clear_attachments" value="1" <%= item.attachments && item.attachments.length ? "" : "disabled" %> />
|
|
<span>Remove attachments</span>
|
|
</label>
|
|
<label class="checkbox-inline">
|
|
<input type="checkbox" name="clear_diagnostics" value="1" />
|
|
<span>Clear diagnostics</span>
|
|
</label>
|
|
<label class="checkbox-inline">
|
|
<input type="checkbox" name="clear_target_metadata" value="1" />
|
|
<span>Clear target metadata</span>
|
|
</label>
|
|
<label class="checkbox-inline">
|
|
<input type="checkbox" name="clear_admin_reply" value="1" />
|
|
<span>Clear public admin reply</span>
|
|
</label>
|
|
<div class="field full button-group">
|
|
<button class="button danger" type="submit">Clean selected data</button>
|
|
</div>
|
|
</form>
|
|
</details>
|
|
|
|
<div class="feedback-comments">
|
|
<h3>Comments and notes</h3>
|
|
<% if (!item.comments.length) { %>
|
|
<p class="hint">No comments or notes yet.</p>
|
|
<% } %>
|
|
<% item.comments.forEach((comment) => { %>
|
|
<article class="feedback-comment feedback-comment-<%= comment.kind %>">
|
|
<strong><%= comment.kind_label %></strong>
|
|
<p><%= comment.body %></p>
|
|
<span class="hint"><%= comment.actor_name || comment.actor_id %> · <%= new Date(comment.created_at).toLocaleString() %><%= comment.visible_to_submitter ? "" : " · private" %></span>
|
|
</article>
|
|
<% }) %>
|
|
</div>
|
|
|
|
<details class="feedback-metadata">
|
|
<summary>Status history</summary>
|
|
<ul class="feedback-history">
|
|
<% item.history.forEach((history) => { %>
|
|
<li><strong><%= history.status_label %></strong> <span class="hint"><%= new Date(history.created_at).toLocaleString() %> · <%= history.actor_name || history.actor_id || "System" %></span><% if (history.note) { %><p><%= history.note %></p><% } %></li>
|
|
<% }) %>
|
|
</ul>
|
|
</details>
|
|
|
|
<form method="post" action="/admin/feedback/<%= item.id %>" class="form-grid feedback-admin-form" data-confirm-mode="modal">
|
|
<div class="field">
|
|
<label>Status</label>
|
|
<select name="status">
|
|
<% feedbackOptions.statuses.forEach((status) => { %>
|
|
<option value="<%= status.value %>" <%= item.status === status.value ? "selected" : "" %>><%= status.label %></option>
|
|
<% }) %>
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label>Category</label>
|
|
<select name="category">
|
|
<% feedbackOptions.categories.forEach((category) => { %>
|
|
<option value="<%= category.value %>" <%= item.category === category.value ? "selected" : "" %>><%= category.label %></option>
|
|
<% }) %>
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label>Severity</label>
|
|
<select name="severity">
|
|
<% feedbackOptions.severities.forEach((severity) => { %>
|
|
<option value="<%= severity.value %>" <%= item.severity === severity.value ? "selected" : "" %>><%= severity.label %></option>
|
|
<% }) %>
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label>Issue link</label>
|
|
<input name="linked_issue" value="<%= item.linked_issue || "" %>" placeholder="Optional URL or issue ID" />
|
|
</div>
|
|
<div class="field">
|
|
<label>Correction link</label>
|
|
<input name="linked_correction" value="<%= item.linked_correction || "" %>" placeholder="Optional OKF/correction link" />
|
|
</div>
|
|
<div class="field full">
|
|
<label>Status note</label>
|
|
<input name="status_note" placeholder="Optional note for status history" />
|
|
</div>
|
|
<div class="field full">
|
|
<label>Reply visible to submitter</label>
|
|
<textarea name="admin_reply" rows="3"><%= item.admin_reply || "" %></textarea>
|
|
</div>
|
|
<div class="field full">
|
|
<label>Private work note</label>
|
|
<textarea name="work_note" rows="3" placeholder="Only admins can see this note."></textarea>
|
|
</div>
|
|
<div class="field full button-group centered">
|
|
<button class="button" type="submit">Save review</button>
|
|
<% if (item.status === "closed") { %>
|
|
<button class="button subtle" type="submit" name="review_action" value="reopen">Reopen</button>
|
|
<% } else { %>
|
|
<button class="button subtle" type="submit" name="review_action" value="finalize">Finalize & Close</button>
|
|
<% } %>
|
|
<button
|
|
class="button danger"
|
|
type="submit"
|
|
formaction="/admin/feedback/<%= item.id %>/delete"
|
|
formmethod="post"
|
|
data-confirm-mode="modal"
|
|
data-confirm-title="Delete feedback permanently"
|
|
data-confirm-text="This permanently deletes the feedback, comments, private notes, status history, and attached screenshot."
|
|
data-confirm-label="Delete feedback"
|
|
>Delete</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</details>
|
|
<% }) %>
|
|
</div>
|
|
<% } %>
|
|
</section>
|
|
<%- include("partials/layout-bottom") %>
|