// Release notes + roadmap (Marty, 2026-09-14): what shipped and what is coming, written in Admin > Releases,
// shown on the public /whats-new page and in a "What's new" card on every member's Overview.
// Storage: DATA_DIR/releases.json { notes: [{id, date, title, body, tags[]}], roadmap: [{id, title, note, status, eta}] }
const fs = require('fs');
const path = require('path');
let FILE = null;
const SITE = 'https://instantadpay.com';
const TAGS = ['new', 'improved', 'fixed'];
const STATUSES = ['planned', 'building', 'done'];
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
function init(opts) { FILE = path.join(opts.dataDir, 'releases.json'); }
function load() { try { return JSON.parse(fs.readFileSync(FILE, 'utf8')); } catch (e) { return { notes: [], roadmap: [] }; } }
function save(d) { fs.writeFileSync(FILE, JSON.stringify(d, null, 1)); }
const newId = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
// body text: plain lines; a line starting with "- " becomes a bullet; blank line = paragraph break
function bodyHtml(text) {
const lines = String(text || '').split(/\r?\n/);
let out = '', list = false, para = [];
const flush = () => { if (para.length) { out += '
' + esc(para.join(' ')) + '
'; para = []; } };
for (const l of lines) {
if (/^\s*-\s+/.test(l)) { flush(); if (!list) { out += ''; list = true; } out += '- ' + esc(l.replace(/^\s*-\s+/, '')) + '
'; continue; }
if (list) { out += '
'; list = false; }
if (!l.trim()) { flush(); continue; }
para.push(l.trim());
}
if (list) out += ''; flush();
return out;
}
function notes() { return load().notes.slice().sort((a, b) => (b.date || '').localeCompare(a.date || '') || (b.ts || 0) - (a.ts || 0)); }
function roadmap() { const order = { building: 0, planned: 1, done: 2 }; return load().roadmap.slice().sort((a, b) => (order[a.status] - order[b.status]) || (a.order || 0) - (b.order || 0)); }
function saveNote(input) {
const d = load(); const title = String(input.title || '').trim().slice(0, 120); if (!title) return { error: 'Give the note a title.' };
const date = /^\d{4}-\d{2}-\d{2}$/.test(String(input.date || '')) ? input.date : new Date().toISOString().slice(0, 10);
const tags = String(input.tags || '').split(',').map(t => t.trim().toLowerCase()).filter(t => TAGS.includes(t));
const n = { id: input.id && d.notes.find(x => x.id === input.id) ? input.id : newId(), date, title, body: String(input.body || '').slice(0, 4000), tags: tags.length ? tags : ['new'], ts: Date.now() };
const i = d.notes.findIndex(x => x.id === n.id); if (i >= 0) d.notes[i] = Object.assign({}, d.notes[i], n); else d.notes.push(n);
save(d); return { ok: true, note: n };
}
function saveRoadmap(input) {
const d = load(); const title = String(input.title || '').trim().slice(0, 120); if (!title) return { error: 'Give the item a title.' };
const status = STATUSES.includes(input.status) ? input.status : 'planned';
const r = { id: input.id && d.roadmap.find(x => x.id === input.id) ? input.id : newId(), title, note: String(input.note || '').slice(0, 600), status, eta: String(input.eta || '').slice(0, 40), order: Number(input.order) || d.roadmap.length + 1, ts: Date.now() };
const i = d.roadmap.findIndex(x => x.id === r.id); if (i >= 0) d.roadmap[i] = Object.assign({}, d.roadmap[i], r); else d.roadmap.push(r);
save(d); return { ok: true, item: r };
}
function remove(kind, id) { const d = load(); const k = kind === 'roadmap' ? 'roadmap' : 'notes'; d[k] = d[k].filter(x => x.id !== id); save(d); return { ok: true }; }
function publicView() { return { notes: notes().slice(0, 30), roadmap: roadmap().filter(r => r.status !== 'done').slice(0, 12), latest: (notes()[0] || {}).date || null }; }
function fmt(d) { try { return new Date(d + 'T12:00:00Z').toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); } catch (e) { return d; } }
const tagChip = t => '' + t + '';
function renderPage() {
const v = publicView();
const desc = 'What changed on InstantAdPay and what is coming next: release notes and the roadmap, updated as things ship.';
let h = 'What\'s new | InstantAdPay'
+ ''
+ ''
+ ''
+ '
Release notes and roadmap
What\'s new, and what\'s next.
' + esc(desc) + '
';
h += '
On the roadmap
';
h += v.roadmap.length ? '
' + v.roadmap.map(r => '
' + r.status + (r.eta ? ' ยท ' + esc(r.eta) : '') + '' + esc(r.title) + '' + (r.note ? '' + esc(r.note) + '' : '') + '
').join('') + '
' : '
Nothing queued right now.
';
h += '
Release notes
';
h += v.notes.length ? v.notes.map(n => '
' + esc(n.title) + n.tags.map(tagChip).join('') + '
' + fmt(n.date) + '
' + bodyHtml(n.body) + '').join('') : '
No notes yet.
';
h += '
Suggestions and bug reports: the chat bubble on any page, or the InstantAdPay Telegram group.
';
h += '
';
return h;
}
module.exports = { init, notes, roadmap, saveNote, saveRoadmap, remove, publicView, renderPage, bodyHtml, TAGS, STATUSES };