Newsletter backup dropped unsubscribe and source columns
The analytics backup and restore round-trip dropped source and unsubscribed entirely. A restore would silently resurrect deleted rows and re-subscribe every opt-out as active.
The column that never existed in code
_init_newsletter_table() created the table with source but never with unsubscribed. The column existed in prod only via a manual ALTER. Since _merge_analytics() calls _init_newsletter_table(), a real restore would have recreated the table without the column and killed every sender, the CSV export and /api/subscribers with no such column: unsubscribed.
The fix
_backup_analytics() now declares and writes both columns. _merge_analytics() restores them. _init_newsletter_table() now creates unsubscribed and backfills both columns.
A restored subscriber whose unsubscribed value cannot be determined comes back as unsubscribed = 1, not 0. Mailing someone who opted out is unrecoverable. Not mailing them is recoverable via /subscribe, which clears the flag on re-signup. The merge prints a loud [ANALYTICS MERGE] WARNING naming the missing columns.
INSERT OR IGNORE is kept on the restore. An existing row is never touched, so a stale backup can never flip a live opt-out back to active.
Admin delete and unsubscribe
/admin/subscribers now has two per-row actions: reversible unsubscribe, and a de-emphasized permanent delete with type-to-confirm. Both routes check session['admin_authenticated'] directly, not _admin_authenticated(), so the X-Admin-Secret header cannot delete.
The admin subscriber rows emit sub.id in a hidden field. The visible # column is loop.index over an ORDER BY signup_date DESC list and would target the wrong row.
Type-to-confirm is verified server-side against the id with hmac.compare_digest. A stale page cannot delete a different row than the one on screen.
Email reaches JS via data-email and dataset. An address containing a quote would otherwise break out once the browser decodes the HTML entity.