Member updates: saved draft (loads into the admin card, Save draft button)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -402,7 +402,7 @@
|
||||
<label class="small">Subject<input id="updSubject" type="text" maxlength="150" placeholder="What is new on InstantAdPay this week"></label>
|
||||
<label class="small">Intro (optional, plain text)<textarea id="updIntro" rows="4" style="width:100%" placeholder="Quick one. Three things shipped since my last note, and the third one is the one to try today."></textarea></label>
|
||||
<label class="small">Audience<select id="updAudience"></select></label>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="button" class="btn small sec" id="updPreview">Preview</button><button type="button" class="btn small sec" id="updTest">Send test to me</button><button type="button" class="btn small" id="updSend">Send</button></div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="button" class="btn small sec" id="updSaveDraft">Save draft</button><button type="button" class="btn small sec" id="updPreview">Preview</button><button type="button" class="btn small sec" id="updTest">Send test to me</button><button type="button" class="btn small" id="updSend">Send</button></div>
|
||||
<pre id="updPre" class="small" hidden style="white-space:pre-wrap;background:rgba(255,255,255,.04);border:1px solid var(--line);border-radius:10px;padding:10px;max-height:320px;overflow:auto"></pre>
|
||||
</div>
|
||||
<div>
|
||||
@@ -480,6 +480,6 @@
|
||||
</div>
|
||||
|
||||
<script src="/assets/common.js?v=20260914a"></script>
|
||||
<script src="/assets/admin.js?v=20260914b"></script>
|
||||
<script src="/assets/admin.js?v=20260914c"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -446,11 +446,14 @@
|
||||
$('updAudience').innerHTML = Object.entries(d.audiences).map(([k, v]) => '<option value="' + k + '">' + esc(v) + ' (' + (d.counts[k] || 0) + ')</option>').join('');
|
||||
$('updNotes').innerHTML = d.notes.length ? d.notes.map(n => '<label class="small" style="display:flex;gap:8px;align-items:flex-start"><input type="checkbox" value="' + esc(n.id) + '"' + (n.fresh ? ' checked' : '') + '><span>' + esc(n.title) + ' <span class="muted">' + esc(n.date) + '</span></span></label>').join('') : '<span class="muted small">No release notes yet.</span>';
|
||||
$('updLog').innerHTML = '<tr><th>When</th><th>Subject</th><th>Audience</th><th>Sent</th></tr>' + (d.sends.length ? d.sends.map(x => '<tr><td class="small">' + new Date(x.ts).toLocaleString() + '</td><td>' + esc(x.subject) + '</td><td class="small">' + esc(d.audiences[x.audience] || x.audience) + '</td><td class="small">' + x.sent + ' of ' + x.total + (x.skipped ? ' · ' + x.skipped + ' opted out' : '') + (x.failed ? ' · ' + x.failed + ' failed' : '') + (x.status === 'running' ? ' · running' : '') + '</td></tr>').join('') : '<tr><td colspan="4" class="muted small">None yet.</td></tr>');
|
||||
if (d.draft && !updDraftLoaded) { updDraftLoaded = true; $('updSubject').value = d.draft.subject || ''; $('updIntro').value = d.draft.intro || ''; if (d.draft.audience) $('updAudience').value = d.draft.audience; $('updNotes').querySelectorAll('input').forEach(i => { i.checked = (d.draft.noteIds || []).includes(i.value); }); $('updSub').textContent += ' · draft loaded (saved ' + new Date(d.draft.savedAt).toLocaleString() + ')'; }
|
||||
if (d.running) setTimeout(loadUpdates, 4000);
|
||||
} catch (e) { $('updSub').textContent = e.message; }
|
||||
}
|
||||
let updDraftLoaded = false;
|
||||
const updInput = () => ({ subject: $('updSubject').value, intro: $('updIntro').value, noteIds: [...$('updNotes').querySelectorAll('input:checked')].map(i => i.value), audience: $('updAudience').value });
|
||||
if ($('updCard')) {
|
||||
$('updSaveDraft').addEventListener('click', busy($('updSaveDraft'), async () => { await api('/api/admin/updates/draft', updInput()); IAP.status('Draft saved.', 'ok'); }));
|
||||
$('updPreview').addEventListener('click', busy($('updPreview'), async () => { const r = await api('/api/admin/updates/preview', updInput()); $('updPre').hidden = false; $('updPre').textContent = 'Subject: ' + r.subject + '\n\n' + r.text; }));
|
||||
$('updTest').addEventListener('click', busy($('updTest'), async () => { const r = await api('/api/admin/updates/send', Object.assign(updInput(), { test: true })); IAP.status('Test sent to ' + r.to + '.', 'ok'); }));
|
||||
$('updSend').addEventListener('click', busy($('updSend'), async () => {
|
||||
|
||||
@@ -1333,7 +1333,11 @@ const server = http.createServer(async (req, res) => {
|
||||
if (p === '/api/admin/updates' && req.method === 'GET') { // member update emails: notes, audiences, log
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const since = updates.lastSentAt();
|
||||
return json(res, 200, Object.assign({ notes: releases.notes().map(n => ({ id: n.id, title: n.title, date: n.date, fresh: !since || (n.created || 0) > since || (n.date && new Date(n.date + 'T12:00:00Z').getTime() > since) })), audiences: updates.AUDIENCES, counts: await updates.counts(), mailer: mailer.hasKey(), lastSentAt: since }, updates.status()));
|
||||
return json(res, 200, Object.assign({ notes: releases.notes().map(n => ({ id: n.id, title: n.title, date: n.date, fresh: !since || (n.created || 0) > since || (n.date && new Date(n.date + 'T12:00:00Z').getTime() > since) })), audiences: updates.AUDIENCES, counts: await updates.counts(), mailer: mailer.hasKey(), lastSentAt: since, draft: updates.draft() }, updates.status()));
|
||||
}
|
||||
if (p === '/api/admin/updates/draft' && req.method === 'POST') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
return json(res, 200, { ok: true, draft: updates.saveDraft(await readBody(req)) });
|
||||
}
|
||||
if (p === '/api/admin/updates/preview' && req.method === 'POST') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
|
||||
+5
-1
@@ -79,6 +79,10 @@ async function send(input, opts) {
|
||||
})();
|
||||
return { ok: true, id: rec.id, total: rec.total };
|
||||
}
|
||||
// one saved draft (subject, intro, noteIds, audience): the admin card loads it, Save draft writes it
|
||||
const DRAFT = () => path.join(R.dataDir, 'updates-draft.json');
|
||||
function draft() { try { return JSON.parse(fs.readFileSync(DRAFT(), 'utf8')); } catch (e) { return null; } }
|
||||
function saveDraft(d) { const v = { subject: String(d.subject || '').slice(0, 150), intro: String(d.intro || '').slice(0, 8000), noteIds: (d.noteIds || []).map(String).slice(0, 40), audience: AUDIENCES[d.audience] ? d.audience : 'optin', savedAt: Date.now() }; try { fs.writeFileSync(DRAFT(), JSON.stringify(v)); } catch (e) {} return v; }
|
||||
function status() { const l = log(); if (running) { const k = l.sends.find(x => x.id === running.id); if (k) Object.assign(k, running); } return { sends: l.sends.slice(0, 12), running: !!running }; }
|
||||
function lastSentAt() { const l = log(); const d = l.sends.find(x => x.status === 'done'); return d ? d.ts : 0; }
|
||||
module.exports = { init, AUDIENCES, counts, compose, send, status, lastSentAt };
|
||||
module.exports = { init, AUDIENCES, counts, compose, send, status, lastSentAt, draft, saveDraft };
|
||||
|
||||
Reference in New Issue
Block a user