Four related changes to how moderation actions get recorded and how new users land after signup.
1. Mod templates split by intent, tied to Community Guidelines
mod_templates gains a duration_hours column. Generic pre-existing rows replaced with:
- Suspend (time-based, duration auto-filled): Spam 24h · Off-topic flame war 48h · Harassment 72h · Impersonation 1 week · Repeated violations 1 month.
- Ban (permanent only): CSAM · Credible threats · Malware · Doxxing · Ban evasion · Exhausted suspension ladder. Each matches a "skip the ladder" category in the Community Guidelines.
Each body cites the specific Community Guidelines section and, where relevant, Terms of Service §4. On the admin user page, the Reason dropdown auto-fills the reason textarea and — for suspensions — auto-computes the duration in days from duration_hours. Last option is always "Other (custom reason)" which clears the textarea + focuses it for free-form entry.
2. Bans are permanent-only; Suspend is time-based
Previously AdminUserActionController::ban accepted a ban_duration, which muddled the conceptual line between ban and suspend. Bans now always write banned_until = NULL. Time-based enforcement uses Suspend. Semantics match the Community Guidelines enforcement ladder: warn → suspend (timed) → permanent ban.
3. Moderator note required on every ban, suspend, and shadow-ban
Each action now requires both a user-facing reason (shown in email + /banned or /suspended) and an internal moderator note (persisted via ModNote::create, visible in Moderator Notes + audit log). Without a note, enforcement context disappears the moment the mod who pressed the button logs off. Applies to both direct admin-user actions and moderation-queue actions. Shadow-ban toggle in the admin sidebar prompts for a note via window.prompt when enabling — removing doesn't require one.
Flash copy now spells it out: "User suspended for 3 days and logged out of all active sessions."
4. Onboarding ends with a "say hello" nudge
After the interests-picker (or Skip), users now land on /onboarding/complete before /feed. The page has one prominent CTA — Introduce yourself — that deep-links to the New Member Intros new-thread form with a pre-filled title ("Hi, I'm @username"). One escape link (Skip to my feed). No enforcement — onboarding_completed_at is already stamped before this page renders so the AuthMiddleware gate no longer funnels the user back.
The new-thread form's title input now honours ?title= in the query string so that pre-fill works. No regression for the common path — the existing draft-restore logic still wins when a draft exists.
. __ ____ ___ ____ _ _
/ /_| ___| / _ \___ \(_)___| |__
| '_ \___ \| | | |__) | / __| '_ \
| (_) |__) | |_| / __/| \__ \ | | |
\___/____/ \___/_____|_|___/_| |_|
D2sk - Sysop