402 lines
12 KiB
Plaintext
402 lines
12 KiB
Plaintext
<% layout('../layout') %>
|
|
|
|
<div class="page">
|
|
<div class="admin-nav">
|
|
<h2>Admin</h2>
|
|
<form method="POST" action="/admin/logout">
|
|
<button type="submit" class="admin-link-button">Log out</button>
|
|
</form>
|
|
</div>
|
|
|
|
<h3>Archive Requests</h3>
|
|
<p class="stage-desc">Requests found in the admin inbox. Approve to whitelist + archive and email the requester the links. Reject asks the requester for more context (optionally with your note) instead of refusing.</p>
|
|
|
|
<% it.archiveRequests.forEach(function(r){ %>
|
|
<div class="req">
|
|
<div class="req-head">
|
|
<div>
|
|
<strong><%= r.subject %></strong>
|
|
<div class="req-from"><%= r.from_name ? r.from_name + ' ' : '' %><<%= r.from_email %>> · <%= r.created_at ? new Date(r.created_at).toISOString().slice(0, 16).replace('T', ' ') : '' %></div>
|
|
</div>
|
|
<div>
|
|
<% if (r.auto_approved) { %><span class="status status--auto">auto-approved</span><% } %>
|
|
<span class="status status--<%= r.status %>"><%= r.status.replace('_', ' ') %></span>
|
|
</div>
|
|
</div>
|
|
|
|
<% if (r.ai_summary) { %><p class="req-summary"><span>AI:</span> <%= r.ai_summary %></p><% } %>
|
|
|
|
<table class="admin-table">
|
|
<thead>
|
|
<tr><th>Video</th><th>Title</th><th>Channel</th><th>Channel ID</th><th>Length</th><th></th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<% r.videos.forEach(function(v){ %>
|
|
<tr>
|
|
<td><a class="a" href="https://youtube.com/watch?v=<%= v.id %>" target="_blank" rel="noopener"><%= v.id %></a></td>
|
|
<td><%= v.title || '?' %></td>
|
|
<td><%= v.channel || '?' %></td>
|
|
<td><%= v.channelId || '?' %></td>
|
|
<td><%= it.fmtLength(v.lengthSeconds) %></td>
|
|
<td>
|
|
<% if (v.isArchived) { %><a class="a" href="/watch?v=<%= v.id %>">archived</a>
|
|
<% } else if (!v.alive) { %><span class="status status--failed">no metadata</span>
|
|
<% } %>
|
|
<% if (v.result && !v.result.success) { %><div class="req-err"><%= v.result.message %></div><% } %>
|
|
</td>
|
|
</tr>
|
|
<% }) %>
|
|
</tbody>
|
|
</table>
|
|
|
|
<details class="req-body">
|
|
<summary>Full email</summary>
|
|
<pre><%= r.body %></pre>
|
|
</details>
|
|
|
|
<% if (r.reply_text) { %>
|
|
<details class="req-body">
|
|
<summary>Reply sent</summary>
|
|
<pre><%= r.reply_text %></pre>
|
|
</details>
|
|
<% } %>
|
|
|
|
<% if (r.error_message) { %><pre class="req-err"><%= r.error_message %></pre><% } %>
|
|
|
|
<% if (r.status === 'pending' || r.status === 'failed') { %>
|
|
<div class="req-actions">
|
|
<form method="POST" action="/admin/requests/<%= r.uuid %>/approve">
|
|
<button type="submit"><%= r.status === 'failed' ? 'Retry' : 'Approve' %></button>
|
|
</form>
|
|
<form method="POST" action="/admin/requests/<%= r.uuid %>/reject" class="admin-form">
|
|
<input type="text" name="note" placeholder="Optional note to requester" />
|
|
<button type="submit" class="secondary">Reject & ask for context</button>
|
|
</form>
|
|
<form method="POST" action="/admin/requests/<%= r.uuid %>/dismiss">
|
|
<button type="submit" class="secondary">Dismiss</button>
|
|
</form>
|
|
</div>
|
|
<% } else if (r.status === 'awaiting_context') { %>
|
|
<div class="req-actions">
|
|
<form method="POST" action="/admin/requests/<%= r.uuid %>/dismiss">
|
|
<button type="submit" class="secondary">Dismiss</button>
|
|
</form>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
<% }) %>
|
|
<% if (it.archiveRequests.length === 0) { %>
|
|
<p class="stage-desc">No archive requests yet.</p>
|
|
<% } %>
|
|
|
|
<h3>Cold Storage Requests</h3>
|
|
<p class="stage-desc">Requests to bring a video back from cold storage. Videos still live on YouTube get a link + "why do you need it?" email first. Approve starts the Glacier restore and emails the requester that an automated email follows within 48 hours. Reject asks for more context.</p>
|
|
|
|
<% it.coldRequests.forEach(function(r){ %>
|
|
<div class="req">
|
|
<div class="req-head">
|
|
<div>
|
|
<strong><%= r.subject %></strong>
|
|
<div class="req-from"><%= r.from_name ? r.from_name + ' ' : '' %><<%= r.from_email %>> · <%= r.created_at ? new Date(r.created_at).toISOString().slice(0, 16).replace('T', ' ') : '' %></div>
|
|
</div>
|
|
<div>
|
|
<% if (r.auto_approved) { %><span class="status status--auto">auto-approved</span><% } %>
|
|
<span class="status status--<%= r.status %>"><%= r.status.replace('_', ' ') %></span>
|
|
</div>
|
|
</div>
|
|
|
|
<% if (r.ai_summary) { %><p class="req-summary"><span>AI:</span> <%= r.ai_summary %></p><% } %>
|
|
|
|
<table class="admin-table">
|
|
<thead>
|
|
<tr><th>Video</th><th>Title</th><th>Channel</th><th>Length</th><th>Size</th><th></th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<% r.videos.forEach(function(v){ %>
|
|
<tr>
|
|
<td><a class="a" href="/watch?v=<%= v.id %>"><%= v.id %></a></td>
|
|
<td><%= v.title || '?' %></td>
|
|
<td><%= v.channel || '?' %></td>
|
|
<td><%= it.fmtLength(v.lengthSeconds) %></td>
|
|
<td><%= it.fmtBytes(v.sizeBytes) %></td>
|
|
<td>
|
|
<% if (v.alive) { %><a class="a" href="https://youtube.com/watch?v=<%= v.id %>" target="_blank" rel="noopener">live on YouTube</a><% } %>
|
|
<% if (v.result) { %><div class="<%= v.result.success ? '' : 'req-err' %>"><%= v.result.message %></div><% } %>
|
|
</td>
|
|
</tr>
|
|
<% }) %>
|
|
</tbody>
|
|
</table>
|
|
|
|
<details class="req-body">
|
|
<summary>Full email</summary>
|
|
<pre><%= r.body %></pre>
|
|
</details>
|
|
|
|
<% if (r.reply_text) { %>
|
|
<details class="req-body">
|
|
<summary>Reply sent</summary>
|
|
<pre><%= r.reply_text %></pre>
|
|
</details>
|
|
<% } %>
|
|
|
|
<% if (r.error_message) { %><pre class="req-err"><%= r.error_message %></pre><% } %>
|
|
|
|
<% if (r.status === 'pending' || r.status === 'failed') { %>
|
|
<div class="req-actions">
|
|
<form method="POST" action="/admin/cold-requests/<%= r.uuid %>/approve">
|
|
<button type="submit"><%= r.status === 'failed' ? 'Retry' : 'Approve restore' %></button>
|
|
</form>
|
|
<form method="POST" action="/admin/cold-requests/<%= r.uuid %>/reject" class="admin-form">
|
|
<input type="text" name="note" placeholder="Optional note to requester" />
|
|
<button type="submit" class="secondary">Reject & ask for context</button>
|
|
</form>
|
|
<form method="POST" action="/admin/cold-requests/<%= r.uuid %>/dismiss">
|
|
<button type="submit" class="secondary">Dismiss</button>
|
|
</form>
|
|
</div>
|
|
<% } else if (r.status === 'awaiting_context') { %>
|
|
<div class="req-actions">
|
|
<form method="POST" action="/admin/cold-requests/<%= r.uuid %>/dismiss">
|
|
<button type="submit" class="secondary">Dismiss</button>
|
|
</form>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
<% }) %>
|
|
<% if (it.coldRequests.length === 0) { %>
|
|
<p class="stage-desc">No cold storage requests yet.</p>
|
|
<% } %>
|
|
|
|
<h3>Auto-approve senders</h3>
|
|
<p class="stage-desc">Requests from these addresses skip review and are archived immediately.</p>
|
|
|
|
<form method="POST" action="/admin/auto-approve" class="admin-form">
|
|
<input type="email" name="email" placeholder="Sender email" required />
|
|
<input type="text" name="note" placeholder="Note (optional)" />
|
|
<button type="submit">Add</button>
|
|
</form>
|
|
|
|
<table class="admin-table">
|
|
<tbody>
|
|
<% it.autoApproveSenders.forEach(function(s){ %>
|
|
<tr>
|
|
<td><%= s.email %></td>
|
|
<td><%= s.note || '' %></td>
|
|
<td>
|
|
<form method="POST" action="/admin/auto-approve/delete">
|
|
<input type="hidden" name="email" value="<%= s.email %>" />
|
|
<button type="submit" class="admin-link-button">Remove</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<% }) %>
|
|
<% if (it.autoApproveSenders.length === 0) { %>
|
|
<tr><td>No auto-approved senders.</td></tr>
|
|
<% } %>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h3>Glacier Restore</h3>
|
|
<p class="stage-desc">Restore a video from Glacier cold storage back to hot storage. AWS retrieval takes up to 48 hours; once it's done, the video is automatically re-uploaded and the requester is emailed.</p>
|
|
|
|
<form method="POST" action="/admin/restore" class="admin-form">
|
|
<input type="text" name="videoId" placeholder="Video ID" required />
|
|
<input type="email" name="requesterEmail" placeholder="Requester email" required />
|
|
<button type="submit">Restore</button>
|
|
</form>
|
|
|
|
<table class="admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Video</th>
|
|
<th>Requester</th>
|
|
<th>Status</th>
|
|
<th>Requested</th>
|
|
<th>Error</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<% it.requests.forEach(function(r){ %>
|
|
<tr>
|
|
<td><a class="a" href="/watch?v=<%= r.videoId %>"><%= r.videoId %></a></td>
|
|
<td><%= r.requester_email %></td>
|
|
<td><span class="status status--<%= r.status %>"><%= r.status %></span></td>
|
|
<td><%= r.created_at ? new Date(r.created_at).toISOString() : '' %></td>
|
|
<td><%= r.error_message || '' %></td>
|
|
</tr>
|
|
<% }) %>
|
|
<% if (it.requests.length === 0) { %>
|
|
<tr><td colspan="5">No restore requests yet.</td></tr>
|
|
<% } %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<style>
|
|
.page {
|
|
margin: 0 auto;
|
|
padding: 5px 10px 10px;
|
|
width: 65%;
|
|
}
|
|
|
|
.admin-nav {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.admin-link-button {
|
|
background: none;
|
|
border: none;
|
|
color: inherit;
|
|
text-decoration-line: underline;
|
|
text-decoration-style: dotted;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
font-size: inherit;
|
|
padding: 0;
|
|
}
|
|
|
|
.stage-desc {
|
|
font-size: 0.9rem;
|
|
color: #aaa;
|
|
}
|
|
|
|
.admin-form {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin: 12px 0 20px;
|
|
}
|
|
|
|
.admin-form input {
|
|
padding: 8px;
|
|
border: 1px solid #ccc;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.admin-form button {
|
|
padding: 8px 16px;
|
|
border: 1px solid #1b1c1f;
|
|
background-color: #1b1c1f;
|
|
color: white;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.admin-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.admin-table th,
|
|
.admin-table td {
|
|
text-align: left;
|
|
padding: 8px;
|
|
border-bottom: 1px solid #eee;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.status {
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
background-color: #eee;
|
|
}
|
|
|
|
.status--reuploaded {
|
|
background-color: #d5f0d5;
|
|
}
|
|
|
|
.status--failed {
|
|
background-color: #f0d5d5;
|
|
}
|
|
|
|
.req {
|
|
border: 1px solid #eee;
|
|
padding: 10px;
|
|
margin: 12px 0;
|
|
}
|
|
|
|
.req-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
.req-from {
|
|
font-size: 0.85rem;
|
|
color: #aaa;
|
|
}
|
|
|
|
.req-summary {
|
|
font-size: 0.9rem;
|
|
margin: 8px 0;
|
|
}
|
|
|
|
.req-summary span {
|
|
color: #aaa;
|
|
}
|
|
|
|
.req-body summary {
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.req-body pre,
|
|
.req-err {
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.req-err {
|
|
color: #a33;
|
|
}
|
|
|
|
.req-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
align-items: flex-start;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.req-actions .admin-form {
|
|
margin: 0;
|
|
}
|
|
|
|
.req-actions button {
|
|
padding: 8px 16px;
|
|
border: 1px solid #1b1c1f;
|
|
background-color: #1b1c1f;
|
|
color: white;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.req-actions button.secondary,
|
|
.admin-form button.secondary {
|
|
background-color: transparent;
|
|
color: inherit;
|
|
}
|
|
|
|
.status--solved,
|
|
.status--auto {
|
|
background-color: #d5f0d5;
|
|
}
|
|
|
|
.status--pending,
|
|
.status--awaiting_context,
|
|
.status--archiving,
|
|
.status--approved {
|
|
background-color: #f0e8c8;
|
|
}
|
|
|
|
.a {
|
|
text-decoration-line: underline;
|
|
text-decoration-style: dotted;
|
|
}
|
|
</style>
|