Files
fluxer/fluxer_admin/src/templates/pages/instance_config.rs
T

2366 lines
108 KiB
Rust

// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::{
api::types::{
AccountIdentityConfigResponse, AccountIdentityMode, AppPublicConfigResponse,
CAPTCHA_COST_RANGE, CAPTCHA_MAX_COUNTER_RANGE, CaptchaConfigResponse,
DOMAIN_MIGRATION_DEFAULT_SALT, DomainMigrationConfigResponse,
EXPERIMENT_MAX_ROLLOUT_COUNTRY_CODES, EXPERIMENT_MAX_TARGETED_USERS,
ExperimentDeliveryConfigResponse, GatewayRolloutConfigResponse, InstanceConfigResponse,
InstanceIntegrationsResponse, InstanceMediaResponse, InstancePolicyResponse,
InstanceRegistrationResponse, LimitConfigResponse, PendingRegistrationResponse,
PushRelayConfigResponse, RegistrationUrlResponse, SsoConfigResponse, TagStyle,
VOICE_P2P_DEFAULT_SALT, VOICE_P2P_MAX_PARTICIPANTS_RANGE, VoiceP2pConfigResponse,
},
config::AdminConfig,
middleware::auth::AuthContext,
templates::{
components::{
badge::{BadgeVariant, badge},
form::{
FORM_INPUT_CLASS, checkbox, csrf_input, danger_button, form_actions,
form_field_group, secondary_button_link, select_input, submit_button, text_input,
textarea_input,
},
page_container::page_header,
section_card::{section_card_simple, section_card_with_description},
},
layout::admin_layout,
pages::instance_billing::premium_billing_section,
},
utils::timestamps::format_admin_timestamp,
};
use maud::{Markup, html};
fn format_percent(value: f64) -> String {
if value.fract() == 0.0 {
format!("{}", value as u64)
} else {
value.to_string()
}
}
fn format_decimal(value: f64) -> String {
if value.fract() == 0.0 {
format!("{}", value as u64)
} else {
value.to_string()
}
}
fn entry_count_hint(count: usize, cap: usize) -> Markup {
html! {
p class="text-xs text-neutral-500" {
(count) " of " (cap) " stored"
@if count >= cap {
" (at the cap; remove an entry before adding another)"
}
}
}
}
fn experiment_rollout_country_codes_field(name: &str, country_codes: &[String]) -> Markup {
html! {
div class="flex flex-col gap-2" {
(text_input(name, "Rollout Countries", &country_codes.join(", "), "SE, NO, FI"))
(entry_count_hint(country_codes.len(), EXPERIMENT_MAX_ROLLOUT_COUNTRY_CODES))
p class="text-xs text-neutral-500" {
"Two-letter ISO 3166-1 country codes, separated by commas or spaces. When set, \
the user rollout percentage only applies to requests from these countries, and \
a request with an unknown country is left out. Leave empty to apply the \
percentage everywhere. The always-on lists, the premium switch and the \
never-on list ignore the country. Invalid entries prevent the save."
}
}
}
}
fn number_field(
name: &str,
label: &str,
value: &str,
min: Option<u32>,
max: Option<u32>,
step: &str,
helper: Option<&str>,
) -> Markup {
form_field_group(
label,
name,
false,
None,
helper,
html! {
input type="number" id=(name) name=(name) step=(step)
value=(value)
min=[min] max=[max] class=(FORM_INPUT_CLASS);
},
)
}
pub fn instance_config_page(
config: &AdminConfig,
auth: &AuthContext,
csrf_token: &str,
instance_config: Option<&InstanceConfigResponse>,
limit_config: Option<&LimitConfigResponse>,
) -> Markup {
let base = &config.base_path;
let content = html! {
(page_header("Instance Configuration", Some("Manage instance-wide settings")))
div class="space-y-10" {
@if let Some(instance_config) = instance_config {
@if instance_config.self_hosted {
(config_group(
"Identity & branding",
"How the instance presents itself to clients and the public discovery document.",
html! {
(app_public_config_section(
base,
csrf_token,
&instance_config.app_public,
instance_config.self_hosted,
))
},
))
}
(config_group(
"Access & accounts",
"Who can sign in and create accounts on this instance.",
html! {
@if instance_config.self_hosted {
(account_identity_section(&instance_config.account_identity))
}
(registration_config_section(
config,
csrf_token,
&instance_config.registration,
instance_config.app_public.registration.collect_date_of_birth,
instance_config.self_hosted,
))
(sso_config_section(base, csrf_token, &instance_config.sso))
},
))
(config_group(
"Bot protection",
"A proof-of-work check on sign-up, login, password reset and a few other abuse-prone actions.",
html! {
(captcha_section(base, csrf_token, &instance_config.captcha))
},
))
@if instance_config.self_hosted {
(config_group(
"Community & policy",
"Community shape, direct messaging, the premium model, and optional embed services.",
html! {
(policy_config_section(
base,
csrf_token,
&instance_config.policy,
&instance_config.app_public.branding.premium_product_name,
))
},
))
(config_group(
"Premium & billing",
"The premium tier's name, Stripe credentials and the prices members pay.",
html! {
(premium_billing_section(
base,
csrf_token,
&instance_config.app_public.branding,
&instance_config.billing,
instance_config.policy.premium_mode,
))
},
))
}
(config_group(
"Runtime integrations",
"Credentials and provider choices that override environment variables at runtime.",
html! {
(integrations_config_section(
base,
csrf_token,
&instance_config.integrations,
instance_config.account_identity.mode,
&instance_config.app_public.branding.product_name,
))
},
))
(config_group(
"Push notifications",
"Consent for the relay that delivers official mobile app notifications.",
html! {
(push_relay_section(base, csrf_token, &instance_config.push_relay))
},
))
(config_group(
"Media & retention",
"Attachment expiry rules that can be changed without editing environment variables.",
html! {
(media_config_section(base, csrf_token, &instance_config.media))
},
))
(config_group(
"Infrastructure & limits",
"Gateway rollout behavior and the limit rules applied to users and guilds.",
html! {
(gateway_rollout_section(base, csrf_token, &instance_config.gateway_rollout))
@if !instance_config.self_hosted {
(domain_migration_section(base, csrf_token, &instance_config.domain_migration))
}
(voice_p2p_section(base, csrf_token, &instance_config.voice_p2p))
(experiment_delivery_section(base, csrf_token, &instance_config.experiment_delivery))
@if let Some(limit_config) = limit_config {
(limit_config_section(base, limit_config))
} @else {
(section_card_simple("Limit Configuration", html! {
p class="text-sm text-red-600" { "Failed to load limit configuration." }
}))
}
},
))
} @else {
(section_card_simple("Instance Configuration", html! {
p class="text-sm text-red-600" { "Failed to load instance configuration." }
}))
@if let Some(limit_config) = limit_config {
(limit_config_section(base, limit_config))
} @else {
(section_card_simple("Limit Configuration", html! {
p class="text-sm text-red-600" { "Failed to load limit configuration." }
}))
}
}
}
};
admin_layout(
config,
auth,
"Instance Configuration",
"instance-config",
None,
content,
)
}
fn config_group(title: &str, description: &str, content: Markup) -> Markup {
html! {
section class="space-y-4" {
div class="space-y-1 border-b border-neutral-200 pb-3" {
h2 class="text-lg font-semibold tracking-tight text-neutral-900" { (title) }
p class="text-sm text-neutral-500" { (description) }
}
div class="space-y-6" {
(content)
}
}
}
}
fn policy_config_section(
base: &str,
csrf_token: &str,
policy: &InstancePolicyResponse,
premium_name: &str,
) -> Markup {
section_card_with_description(
"Community & Policy",
"Control whether this instance runs as a single community, whether direct messages and \
friends are available, which premium model applies, and which optional embed services \
are enabled.",
html! {
div class="space-y-8" {
(single_community_form(base, csrf_token, policy))
(direct_messages_form(base, csrf_token, policy))
(premium_mode_form(base, csrf_token, policy, premium_name))
(community_creation_form(base, csrf_token, policy))
(services_form(base, csrf_token, policy))
}
},
)
}
fn single_community_form(base: &str, csrf_token: &str, policy: &InstancePolicyResponse) -> Markup {
let status = if policy.single_community_enabled {
("Enabled", BadgeVariant::Success)
} else {
("Disabled", BadgeVariant::Default)
};
html! {
div class="space-y-4" {
div class="flex flex-wrap items-center gap-2" {
h3 class="text-sm font-semibold text-neutral-900" { "Single community" }
(badge(status.0, status.1))
}
@if let Some(guild_id) = policy.single_community_guild_id.as_deref() {
p class="break-all text-xs text-neutral-500" { "Community guild ID: " (guild_id) }
}
@if policy.single_community_enabled {
p class="text-sm text-neutral-500" {
"This instance funnels every member into a single community. You can turn this \
off and on again from here. The community itself is kept either way."
}
form method="post" action={(base) "/instance-config?action=disable_single_community"} {
(csrf_input(csrf_token))
(form_actions(html! {
(danger_button("Disable single-community mode"))
}))
}
} @else if policy.single_community_guild_id.is_some() {
p class="text-sm text-neutral-500" {
"Single-community mode is off. Turning it on again reuses the community above \
if it still exists, otherwise a new one is created."
}
form method="post" action={(base) "/instance-config?action=enable_single_community"} {
(csrf_input(csrf_token))
(form_actions(html! {
(submit_button("Enable single-community mode"))
}))
}
} @else {
p class="text-sm text-neutral-500" {
"Single-community mode is off. It can only be turned on for the first time \
from the self-host setup wizard."
}
}
}
}
}
fn direct_messages_form(base: &str, csrf_token: &str, policy: &InstancePolicyResponse) -> Markup {
html! {
div class="space-y-4 border-t border-neutral-200 pt-6" {
div class="flex flex-wrap items-center gap-2" {
h3 class="text-sm font-semibold text-neutral-900" { "Direct messages & friends" }
@if policy.direct_messages_locked {
(badge("Locked", BadgeVariant::Warning))
}
}
@if policy.direct_messages_locked {
p class="text-sm text-neutral-500" {
@if policy.direct_messages_disabled {
"Direct messages and friends are disabled and locked for this instance."
} @else {
"Direct messages and friends are enabled and locked for this instance."
}
}
} @else {
form method="post" action={(base) "/instance-config?action=update_policy"} {
(csrf_input(csrf_token))
div class="space-y-4" {
(select_input("policy_direct_messages_disabled", "Direct messages & friends", &[
("false", "Enabled"),
("true", "Disabled"),
], if policy.direct_messages_disabled { "true" } else { "false" }))
p class="text-xs text-neutral-500" {
"Re-enabling direct messages after they have been disabled locks this \
setting permanently."
}
(form_actions(html! {
(submit_button("Save direct message policy"))
}))
}
}
}
}
}
}
fn premium_mode_form(
base: &str,
csrf_token: &str,
policy: &InstancePolicyResponse,
premium_name: &str,
) -> Markup {
let mirror_label = format!("Mirror (Free and {premium_name} tiers)");
let everyone_label = format!("Everyone (every member gets {premium_name} limits)");
html! {
div class="space-y-4 border-t border-neutral-200 pt-6" {
h3 class="text-sm font-semibold text-neutral-900" { "Premium model" }
form method="post" action={(base) "/instance-config?action=update_policy"} {
(csrf_input(csrf_token))
div class="space-y-4" {
(select_input("policy_premium_mode", "Premium model", &[
("mirror", mirror_label.as_str()),
("everyone", everyone_label.as_str()),
], policy.premium_mode.as_str()))
(form_actions(html! {
(submit_button("Save premium model"))
}))
}
}
}
}
}
fn community_creation_form(
base: &str,
csrf_token: &str,
policy: &InstancePolicyResponse,
) -> Markup {
html! {
div id="community-creation" class="space-y-4 border-t border-neutral-200 pt-6" {
h3 class="text-sm font-semibold text-neutral-900" { "Community creation" }
form method="post" action={(base) "/instance-config?action=update_policy"} {
(csrf_input(csrf_token))
div class="space-y-4" {
(select_input("policy_guild_create_access", "Who can create communities", &[
("true", "Everyone"),
("false", "Restricted"),
], if policy.guild_create_access { "true" } else { "false" }))
p class="text-xs text-neutral-500" {
"When restricted, only admins with the wildcard ACL and users matched by a "
a href={(base) "/limit-config"} class="text-blue-600 hover:underline" {
"limit rule"
}
" that grants Community Creation Access can create communities."
}
(form_actions(html! {
(submit_button("Save community creation policy"))
}))
}
}
}
}
}
fn service_select(name: &str, label: &str, override_value: Option<bool>, resolved: bool) -> Markup {
let selected = match override_value {
None => "inherit",
Some(true) => "on",
Some(false) => "off",
};
let resolved_text = if resolved {
"Effective: enabled"
} else {
"Effective: disabled"
};
html! {
div class="space-y-2" {
(select_input(name, label, &[
("inherit", "Inherit env default"),
("on", "Force on"),
("off", "Force off"),
], selected))
p class="text-xs text-neutral-500" { (resolved_text) }
}
}
}
fn services_form(base: &str, csrf_token: &str, policy: &InstancePolicyResponse) -> Markup {
let available = &policy.services_available;
if !available.gif && !available.youtube && !available.bluesky {
return html! {
div class="space-y-4 border-t border-neutral-200 pt-6" {
h3 class="text-sm font-semibold text-neutral-900" { "Optional services" }
p class="text-sm text-neutral-500" {
"No optional embed services are available in this environment."
}
}
};
}
html! {
div class="space-y-4 border-t border-neutral-200 pt-6" {
h3 class="text-sm font-semibold text-neutral-900" { "Optional services" }
p class="text-sm text-neutral-500" {
"Override the environment default for each optional embed service. \
\"Inherit env default\" keeps the value configured for this deployment."
}
form method="post" action={(base) "/instance-config?action=update_policy"} {
(csrf_input(csrf_token))
div class="space-y-4" {
div class="grid grid-cols-1 gap-4 sm:grid-cols-3" {
@if available.gif {
(service_select(
"policy_service_gif",
"GIF picker",
policy.services.gif_enabled,
policy.services_resolved.gif_enabled,
))
}
@if available.youtube {
(service_select(
"policy_service_youtube",
"YouTube embeds",
policy.services.youtube_enabled,
policy.services_resolved.youtube_enabled,
))
}
@if available.bluesky {
(service_select(
"policy_service_bluesky",
"Bluesky embeds",
policy.services.bluesky_enabled,
policy.services_resolved.bluesky_enabled,
))
}
}
(form_actions(html! {
(submit_button("Save optional services"))
}))
}
}
}
}
}
fn secret_badge(label: &str, is_set: bool) -> Markup {
if is_set {
badge(&format!("{label} set"), BadgeVariant::Success)
} else {
badge(&format!("{label} missing"), BadgeVariant::Default)
}
}
fn password_input(name: &str, label: &str, helper: Option<&str>) -> Markup {
form_field_group(
label,
name,
false,
None,
helper,
html! {
input type="password" id=(name) name=(name) value="" class=(FORM_INPUT_CLASS)
autocomplete="new-password";
},
)
}
fn account_identity_section(account_identity: &AccountIdentityConfigResponse) -> Markup {
let description = match account_identity.mode {
AccountIdentityMode::Username => {
"Members sign in with a username and password. The instance never collects an email \
address. A member who forgets their password uses their recovery kit or a reset link \
from an admin."
}
AccountIdentityMode::Email => "Members sign in with an email address and password.",
};
section_card_with_description(
"Sign-in Method",
"How members identify themselves when they sign in.",
html! {
div class="space-y-3" {
div class="flex flex-wrap items-center gap-2" {
h3 class="text-sm font-semibold text-neutral-900" {
(account_identity.mode.label())
}
@match account_identity.locked {
Some(true) => (badge("Fixed", BadgeVariant::Default)),
Some(false) => (badge("Not fixed yet", BadgeVariant::Warning)),
None => {}
}
}
p class="text-sm text-neutral-600" { (description) }
@if !account_identity.mode.is_username() {
div class="flex flex-wrap items-center gap-2" {
h3 class="text-sm font-semibold text-neutral-900" {
(account_identity.tag_style.label())
}
}
p class="text-sm text-neutral-600" {
@match account_identity.tag_style {
TagStyle::None => {
"Each name belongs to one person and is shown without a tag."
}
TagStyle::Random => {
"Names have a random tag, like alex#4821, so several people can share a name."
}
}
}
}
p class="text-xs text-neutral-500" {
@if account_identity.mode.is_username() {
"The sign-in method is chosen during setup. It cannot be changed once setup is complete or the first account exists."
} @else {
"The sign-in method and the username tags are chosen during setup. They cannot be changed once setup is complete or the first account exists."
}
}
}
},
)
}
fn integrations_config_section(
base: &str,
csrf_token: &str,
integrations: &InstanceIntegrationsResponse,
account_identity: AccountIdentityMode,
product_name: &str,
) -> Markup {
let smtp_port = integrations
.email
.smtp
.port
.map(|port| port.to_string())
.unwrap_or_else(|| "587".to_owned());
section_card_with_description(
"Runtime Integrations",
"Configure optional providers without changing environment variables. Blank secret fields keep the current value.",
html! {
form method="post" action={(base) "/instance-config?action=update_integrations"} {
(csrf_input(csrf_token))
div class="space-y-8" {
div class="space-y-4" {
div class="flex flex-wrap items-center gap-2" {
h3 class="text-sm font-semibold text-neutral-900" { "GIF provider" }
(secret_badge("KLIPY key", integrations.gif.klipy_api_key_set))
}
div class="grid grid-cols-1 gap-4" {
(password_input("integration_klipy_api_key", "KLIPY API key", Some("Leave blank to keep the current key.")))
}
}
div class="space-y-4 border-t border-neutral-200 pt-6" {
div class="flex flex-wrap items-center gap-2" {
h3 class="text-sm font-semibold text-neutral-900" { "YouTube Data API" }
(secret_badge("API key", integrations.youtube.api_key_set))
}
(password_input("integration_youtube_api_key", "YouTube API key", Some("Leave blank to keep the current key.")))
}
@if !account_identity.is_username() {
div class="space-y-4 border-t border-neutral-200 pt-6" {
div class="flex flex-wrap items-center gap-2" {
h3 class="text-sm font-semibold text-neutral-900" { "Email delivery" }
@if integrations.email.effective_enabled {
(badge("Effective: enabled", BadgeVariant::Success))
} @else {
(badge("Effective: disabled", BadgeVariant::Default))
}
@if integrations.email.effective_disable_new_ip_authorization {
(badge("IP auth disabled", BadgeVariant::Warning))
} @else {
(badge("IP auth required", BadgeVariant::Default))
}
(secret_badge("SMTP password", integrations.email.smtp.password_set))
}
input type="hidden" name="integration_email_present" value="1";
(checkbox("integration_email_enabled", "true", "Enable email delivery", integrations.email.effective_enabled, true))
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(text_input(
"integration_email_from_email",
"From email",
integrations.email.from_email.as_deref().unwrap_or(""),
"[email protected]",
))
(text_input(
"integration_email_from_name",
"From name",
integrations.email.from_name.as_deref().unwrap_or(""),
product_name,
))
(text_input(
"integration_smtp_host",
"SMTP host",
integrations.email.smtp.host.as_deref().unwrap_or(""),
"smtp.example.com",
))
(text_input(
"integration_smtp_port",
"SMTP port",
&smtp_port,
"587",
))
(text_input(
"integration_smtp_username",
"SMTP username",
integrations.email.smtp.username.as_deref().unwrap_or(""),
"[email protected]",
))
(password_input("integration_smtp_password", "SMTP password", Some("Leave blank to keep the current password.")))
}
(checkbox("integration_smtp_secure", "true", "Use TLS", integrations.email.smtp.secure.unwrap_or(true), true))
(checkbox("integration_email_disable_new_ip_authorization", "true", "Disable new IP login authorization", integrations.email.disable_new_ip_authorization, true))
div class="flex flex-wrap gap-2" {
button type="submit"
formaction={(base) "/instance-config?action=test_smtp"}
class="inline-flex w-fit items-center justify-center gap-2 rounded-lg border border-neutral-300 bg-neutral-50 px-4 py-2 font-medium text-base text-neutral-700 transition-all duration-150 hover:border-neutral-400 hover:text-neutral-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white" {
span { "Test SMTP connection" }
}
}
}
}
div class="space-y-4 border-t border-neutral-200 pt-6" {
div class="flex flex-wrap items-center gap-2" {
h3 class="text-sm font-semibold text-neutral-900" { "Bluesky OAuth" }
@if integrations.bluesky.effective_enabled {
(badge("Effective: enabled", BadgeVariant::Success))
} @else {
(badge("Effective: disabled", BadgeVariant::Default))
}
(badge(&format!("{} signing key(s)", integrations.bluesky.key_count), BadgeVariant::Default))
}
(checkbox("integration_bluesky_enabled", "true", "Enable Bluesky OAuth", integrations.bluesky.effective_enabled, true))
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(text_input(
"integration_bluesky_client_name",
"Client name",
integrations.bluesky.client_name.as_deref().unwrap_or(""),
"Fluxer",
))
(text_input(
"integration_bluesky_client_uri",
"Client URL",
integrations.bluesky.client_uri.as_deref().unwrap_or(""),
"https://example.com",
))
(text_input(
"integration_bluesky_logo_uri",
"Logo URL",
integrations.bluesky.logo_uri.as_deref().unwrap_or(""),
"https://example.com/logo.png",
))
(text_input(
"integration_bluesky_tos_uri",
"Terms URL",
integrations.bluesky.tos_uri.as_deref().unwrap_or(""),
"https://example.com/terms",
))
(text_input(
"integration_bluesky_policy_uri",
"Privacy URL",
integrations.bluesky.policy_uri.as_deref().unwrap_or(""),
"https://example.com/privacy",
))
(text_input("integration_bluesky_key_id", "New signing key ID", "", "atproto-key-1"))
(password_input("integration_bluesky_private_key", "New private key", Some("Provide this only when adding or replacing runtime Bluesky keys.")))
}
}
(form_actions(html! {
(submit_button("Save runtime integrations"))
}))
}
}
},
)
}
fn media_config_section(base: &str, csrf_token: &str, media: &InstanceMediaResponse) -> Markup {
let decay = &media.attachment_decay;
let effective = &decay.effective;
let enabled = decay.enabled.unwrap_or(effective.enabled);
let min_size_mb = format_decimal(decay.min_size_mb.unwrap_or(effective.min_size_mb));
let max_size_mb = format_decimal(decay.max_size_mb.unwrap_or(effective.max_size_mb));
let max_eligible_size_mb = format_decimal(
decay
.max_eligible_size_mb
.unwrap_or(effective.max_eligible_size_mb),
);
let min_lifetime_days = decay
.min_lifetime_days
.unwrap_or(effective.min_lifetime_days)
.to_string();
let max_lifetime_days = decay
.max_lifetime_days
.unwrap_or(effective.max_lifetime_days)
.to_string();
let curve = format_decimal(decay.curve.unwrap_or(effective.curve));
let renew_threshold_days = decay
.renew_threshold_days
.unwrap_or(effective.renew_threshold_days)
.to_string();
let renew_window_days = decay
.renew_window_days
.unwrap_or(effective.renew_window_days)
.to_string();
section_card_with_description(
"Media Expiry",
"Expire eligible attachments using size-based lifetimes. This is disabled by default and applies at runtime.",
html! {
form method="post" action={(base) "/instance-config?action=update_media"} {
(csrf_input(csrf_token))
div class="space-y-6" {
div class="flex flex-wrap items-center gap-2" {
@if effective.enabled {
(badge("Effective: enabled", BadgeVariant::Success))
} @else {
(badge("Effective: disabled", BadgeVariant::Default))
}
}
(checkbox("media_attachment_decay_enabled", "true", "Enable attachment expiry", enabled, true))
div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3" {
(number_field(
"media_attachment_decay_min_size_mb",
"Small file threshold (MB)",
&min_size_mb,
Some(1),
Some(1_000_000),
"0.1",
Some("Attachments at or below this size keep the maximum lifetime."),
))
(number_field(
"media_attachment_decay_max_size_mb",
"Large file threshold (MB)",
&max_size_mb,
Some(1),
Some(1_000_000),
"0.1",
Some("Attachments at or above this size get the minimum lifetime."),
))
(number_field(
"media_attachment_decay_max_eligible_size_mb",
"Maximum eligible size (MB)",
&max_eligible_size_mb,
Some(1),
Some(1_000_000),
"0.1",
Some("Attachments above this size are not managed by expiry."),
))
(number_field(
"media_attachment_decay_min_lifetime_days",
"Minimum lifetime (days)",
&min_lifetime_days,
Some(1),
Some(36_500),
"1",
None,
))
(number_field(
"media_attachment_decay_max_lifetime_days",
"Maximum lifetime (days)",
&max_lifetime_days,
Some(1),
Some(36_500),
"1",
None,
))
(number_field(
"media_attachment_decay_curve",
"Expiry curve",
&curve,
Some(0),
Some(1),
"0.05",
Some("0 is linear. 1 is logarithmic."),
))
(number_field(
"media_attachment_decay_renew_threshold_days",
"Renew threshold (days)",
&renew_threshold_days,
Some(1),
Some(365),
"1",
Some("Viewed attachments renew when expiry is this close."),
))
(number_field(
"media_attachment_decay_renew_window_days",
"Renew window (days)",
&renew_window_days,
Some(1),
Some(365),
"1",
Some("Renewed attachments are extended by this many days."),
))
}
(form_actions(html! {
(submit_button("Save media expiry"))
}))
}
}
},
)
}
fn app_public_config_section(
base: &str,
csrf_token: &str,
app_public: &AppPublicConfigResponse,
self_hosted: bool,
) -> Markup {
let setup_helper = if self_hosted {
"When this is unchecked, the web app shows the self-host setup wizard."
} else {
"Hosted instances are treated as already configured by clients."
};
section_card_with_description(
"Public App Identity",
"Configure the public product name, client-visible brand assets, and initial setup state returned by the instance discovery document.",
html! {
div class="space-y-8" {
form method="post" action={(base) "/instance-config?action=update_app_public"} {
(csrf_input(csrf_token))
div class="space-y-6" {
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(text_input(
"app_product_name",
"Product Name",
&app_public.branding.product_name,
"Fluxer",
))
(text_input(
"app_theme_color",
"Theme Color",
app_public.branding.theme_color.as_deref().unwrap_or(""),
"#5865f2",
))
}
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(text_input(
"app_icon_url",
"Icon URL",
app_public.branding.icon_url.as_deref().unwrap_or(""),
"https://example.com/icon.png",
))
(text_input(
"app_symbol_url",
"Symbol URL",
app_public.branding.symbol_url.as_deref().unwrap_or(""),
"https://example.com/symbol.png",
))
(text_input(
"app_logo_url",
"Logo URL",
app_public.branding.logo_url.as_deref().unwrap_or(""),
"https://example.com/logo.png",
))
(text_input(
"app_wordmark_url",
"Wordmark URL",
app_public.branding.wordmark_url.as_deref().unwrap_or(""),
"https://example.com/wordmark.png",
))
(text_input(
"app_favicon_url",
"Favicon URL",
app_public.branding.favicon_url.as_deref().unwrap_or(""),
"https://example.com/favicon.ico",
))
(text_input(
"app_status_page_url",
"Status page URL",
app_public.branding.status_page_url.as_deref().unwrap_or(""),
"https://fluxerstatus.com",
))
(text_input(
"app_status_page_incident_history_url",
"Status page history URL",
app_public.branding.status_page_incident_history_url.as_deref().unwrap_or(""),
"https://fluxerstatus.com/history",
))
}
div class="space-y-2" {
(checkbox(
"app_setup_configured",
"true",
"Setup complete",
app_public.setup.configured,
true,
))
p class="text-xs text-neutral-500" { (setup_helper) }
}
(form_actions(html! {
(submit_button("Save Public App Identity"))
}))
}
}
div class="space-y-4 border-t border-neutral-200 pt-6" {
h3 class="text-sm font-semibold text-neutral-900" { "Legal Documents" }
form method="post" action={(base) "/instance-config?action=update_app_legal"} {
(csrf_input(csrf_token))
div class="space-y-4" {
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(text_input(
"app_terms_url",
"Terms URL",
app_public.legal.terms_url.as_deref().unwrap_or(""),
"https://example.com/terms",
))
(text_input(
"app_privacy_url",
"Privacy URL",
app_public.legal.privacy_url.as_deref().unwrap_or(""),
"https://example.com/privacy",
))
(text_input(
"app_guidelines_url",
"Community Guidelines URL",
app_public.legal.guidelines_url.as_deref().unwrap_or(""),
"https://example.com/guidelines",
))
}
p class="text-xs text-neutral-500" {
"Registration asks for agreement only to the terms and privacy documents configured here. Leave both blank to hide legal consent on self-hosted registration."
}
p class="text-xs text-neutral-500" {
"The community guidelines URL is linked from the app, the report flow and enforcement emails. Use an absolute http or https URL. Leave it blank to hide those links."
}
(form_actions(html! {
(submit_button("Save Legal Documents"))
}))
}
}
}
}
},
)
}
fn gateway_rollout_section(
base: &str,
csrf_token: &str,
gateway_rollout: &GatewayRolloutConfigResponse,
) -> Markup {
section_card_with_description(
"Gateway Rollout Configuration",
"Control session and guild rollout percentages, NATS timeouts, and \
concurrency limits for gateway nodes. Changes propagate instantly \
via NATS.",
html! {
form method="post" action={(base) "/instance-config?action=update_gateway_rollout"} {
(csrf_input(csrf_token))
div class="space-y-6" {
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(number_field(
"gateway_rollout_session_rollout_percentage",
"Session Rollout (%)", &format_percent(gateway_rollout.session_rollout_percentage),
Some(0), Some(100), "1",
Some("Percentage of sessions allowed to start (0-100)."),
))
(select_input("gateway_rollout_session_rollout_mode", "Session Rollout Mode", &[
("modulo", "Modulo"),
("random", "Random"),
], gateway_rollout.session_rollout_mode.as_str()))
}
(number_field(
"gateway_rollout_guild_rollout_percentage",
"Guild Rollout (%)", &format_percent(gateway_rollout.guild_rollout_percentage),
Some(0), Some(100), "1",
Some("Percentage of guilds allowed to start (0-100). Uses modulo mode."),
))
h3 class="text-sm font-semibold text-neutral-900" { "RPC and Concurrency" }
div class="grid grid-cols-1 gap-4 sm:grid-cols-3" {
(number_field(
"gateway_rollout_rpc_request_timeout_ms",
"RPC Request Timeout (ms)", &gateway_rollout.rpc_request_timeout_ms.to_string(),
Some(1000), Some(60000), "1",
Some("Timeout for gateway-to-API RPC calls."),
))
(number_field(
"gateway_rollout_max_concurrent_session_starts",
"Max Concurrent Session Starts", &gateway_rollout.max_concurrent_session_starts.to_string(),
Some(1), None, "1",
Some("Per-node concurrency limit."),
))
(number_field(
"gateway_rollout_max_concurrent_guild_starts",
"Max Concurrent Guild Starts", &gateway_rollout.max_concurrent_guild_starts.to_string(),
Some(1), None, "1",
Some("Per-node concurrency limit."),
))
}
h3 class="text-sm font-semibold text-neutral-900" { "Voice E2EE" }
(select_input("gateway_rollout_voice_e2ee_scope", "Enforcement scope", &[
("guild_feature_only", "Guild feature only (require VOICE_E2EE on the guild; DMs/GDMs never E2EE)"),
("platform_wide", "Platform-wide (every guild voice channel, DM, and GDM)"),
], gateway_rollout.voice_e2ee_scope.as_str()))
(form_actions(html! {
(submit_button("Save Gateway Rollout Configuration"))
}))
}
}
},
)
}
fn push_relay_section(
base: &str,
csrf_token: &str,
push_relay: &PushRelayConfigResponse,
) -> Markup {
let status = if push_relay.relay_consent_accepted {
("Accepted", BadgeVariant::Success)
} else {
("Not accepted", BadgeVariant::Default)
};
let accepted_at =
format_optional_admin_timestamp(push_relay.relay_consent_accepted_at.as_deref(), "Never");
let accepted_by = push_relay
.relay_consent_accepted_by
.as_deref()
.unwrap_or("Nobody");
section_card_with_description(
"Push Relay",
"Official mobile app notifications travel through Fluxer's relay to Apple and Google. \
The relay delivers them only after an operator accepts its privacy notice.",
html! {
form method="post" action={(base) "/instance-config?action=update_push_relay"} {
(csrf_input(csrf_token))
div class="space-y-6" {
div class="flex flex-wrap items-center gap-2" {
h3 class="text-sm font-semibold text-neutral-900" { "Relay consent" }
(badge(status.0, status.1))
}
(checkbox(
"push_relay_consent_accepted",
"true",
"Accept the push relay supplemental privacy notice",
push_relay.relay_consent_accepted,
true,
))
p class="text-xs text-neutral-500" {
"Until this is accepted official mobile app notifications are dropped. \
Self-hosted UnifiedPush and ntfy endpoints never reach the relay and are \
unaffected. "
a href="https://fluxer.com/push-relay" target="_blank" rel="noreferrer"
class="text-neutral-900 underline decoration-neutral-300 hover:text-neutral-600 hover:decoration-neutral-500" {
"Read the notice"
}
}
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(form_field_group("Accepted at", "push_relay_consent_accepted_at", false, None, None,
html! {
input type="text" id="push_relay_consent_accepted_at"
value=(accepted_at)
disabled class=(FORM_INPUT_CLASS);
},
))
(form_field_group("Accepted by user ID", "push_relay_consent_accepted_by", false, None, None,
html! {
input type="text" id="push_relay_consent_accepted_by"
value=(accepted_by)
disabled class=(FORM_INPUT_CLASS);
},
))
}
(form_actions(html! {
(submit_button("Save Push Relay Settings"))
}))
}
}
},
)
}
fn domain_migration_section(
base: &str,
csrf_token: &str,
domain_migration: &DomainMigrationConfigResponse,
) -> Markup {
let status = if domain_migration.enabled {
("Live", BadgeVariant::Success)
} else {
("Inert", BadgeVariant::Default)
};
let included_user_ids = domain_migration.included_user_ids.join("\n");
let excluded_user_ids = domain_migration.excluded_user_ids.join("\n");
section_card_with_description(
"Domain Migration",
"Moves web clients of the official instance from the legacy web app origin to the new \
one. Selected accounts copy their local data across and continue on the new origin. \
Clients of other instances read this configuration and ignore it.",
html! {
form method="post" action={(base) "/instance-config?action=update_domain_migration"} {
(csrf_input(csrf_token))
div class="space-y-6" {
div class="flex flex-wrap items-center gap-2" {
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
(badge(status.0, status.1))
span class="text-xs text-neutral-500" {
"Config version " (domain_migration.config_version)
}
}
(checkbox(
"domain_migration_enabled",
"true",
"Move selected web clients to the new origin",
domain_migration.enabled,
true,
))
p class="text-xs text-neutral-500" {
"Off is the safe state and the kill switch. With this unchecked no client \
starts a migration and clients that already migrated stop forwarding the \
legacy origin, so the rollout and targeting fields below have no effect at all."
}
h3 class="text-sm font-semibold text-neutral-900" { "Installed apps" }
(checkbox(
"domain_migration_standalone_forwarding",
"true",
"Forward installed desktop web apps to the new origin",
domain_migration.standalone_forwarding,
true,
))
p class="text-xs text-neutral-500" {
"Leave this off until the manifest scope extension and the association file \
are live and verified. While it is off, installed Chromium desktop apps copy \
their data across but stay on the legacy origin and offer to install the new \
app. Installed mobile and Safari apps never forward either way."
}
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
(number_field(
"domain_migration_rollout_basis_points",
"Rollout (basis points)",
&domain_migration.rollout_basis_points.to_string(),
Some(0), Some(10000), "1",
Some("Share of logged-in users bucketed into the migration, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."),
))
(number_field(
"domain_migration_anonymous_rollout_basis_points",
"Anonymous rollout (basis points)",
&domain_migration.anonymous_rollout_basis_points.to_string(),
Some(0), Some(10000), "1",
Some("Share of logged-out devices sent to the new origin, in basis points. Each device is bucketed on its own random ID."),
))
(experiment_rollout_country_codes_field(
"domain_migration_rollout_country_codes",
&domain_migration.rollout_country_codes,
))
div class="flex flex-col gap-2" {
(text_input(
"domain_migration_rollout_salt",
"Rollout Salt",
&domain_migration.rollout_salt,
DOMAIN_MIGRATION_DEFAULT_SALT,
))
p class="text-xs text-neutral-500" {
"Seeds the bucketing hash for users and devices. Changing it reshuffles \
which users and devices fall inside the percentages above. Leave it \
alone to keep the current cohort stable."
}
}
div class="flex flex-col gap-2" {
(textarea_input(
"domain_migration_included_user_ids",
"Always-on User IDs",
"1500000000000000001\n1500000000000000002",
&included_user_ids,
4,
false,
))
(entry_count_hint(
domain_migration.included_user_ids.len(),
EXPERIMENT_MAX_TARGETED_USERS,
))
p class="text-xs text-neutral-500" {
"One snowflake per line, or comma separated. These users are targeted \
regardless of the percentage above. IDs must contain 1 to 20 decimal \
digits. Invalid entries prevent the save. Blank entries and duplicate \
IDs are ignored."
}
}
div class="flex flex-col gap-2" {
(checkbox(
"domain_migration_include_premium_users",
"true",
"Include premium users",
domain_migration.include_premium_users,
true,
))
p class="text-xs text-neutral-500" {
"Includes every account with active premium perks, regardless of the \
percentage above. The never-on list still wins."
}
}
div class="flex flex-col gap-2" {
(textarea_input(
"domain_migration_included_guild_ids",
"Always-on Guild IDs",
"1500000000000000005\n1500000000000000006",
&domain_migration.included_guild_ids.join("\n"),
4,
false,
))
(entry_count_hint(
domain_migration.included_guild_ids.len(),
EXPERIMENT_MAX_TARGETED_USERS,
))
p class="text-xs text-neutral-500" {
"Same format, with guild IDs. Every member of a listed guild is \
included regardless of the percentage above, unless the user is \
in the never-on list."
}
}
div class="flex flex-col gap-2" {
(textarea_input(
"domain_migration_excluded_user_ids",
"Never-on User IDs",
"1500000000000000003\n1500000000000000004",
&excluded_user_ids,
4,
false,
))
(entry_count_hint(
domain_migration.excluded_user_ids.len(),
EXPERIMENT_MAX_TARGETED_USERS,
))
p class="text-xs text-neutral-500" {
"Same format. Exclusion wins over both the always-on list and the \
percentage. It stops new migrations only. A user who already moved \
stays on the new origin."
}
}
(form_actions(html! {
(submit_button("Save Domain Migration Configuration"))
}))
}
}
},
)
}
fn voice_p2p_section(base: &str, csrf_token: &str, voice_p2p: &VoiceP2pConfigResponse) -> Markup {
let status = if voice_p2p.enabled {
("Live", BadgeVariant::Success)
} else {
("Inert", BadgeVariant::Default)
};
let included_user_ids = voice_p2p.included_user_ids.join("\n");
let excluded_user_ids = voice_p2p.excluded_user_ids.join("\n");
section_card_with_description(
"Peer-to-peer voice",
"Lets selected users start small voice calls that connect the participants directly \
instead of through the voice servers. A peer-to-peer call only moves to the voice \
servers when a participant switches it or its region changes.",
html! {
form method="post" action={(base) "/instance-config?action=update_voice_p2p"} {
(csrf_input(csrf_token))
div class="space-y-6" {
div class="flex flex-wrap items-center gap-2" {
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
(badge(status.0, status.1))
span class="text-xs text-neutral-500" {
"Config version " (voice_p2p.config_version)
}
}
(checkbox(
"voice_p2p_enabled",
"true",
"Let the selected users start peer-to-peer calls",
voice_p2p.enabled,
true,
))
p class="text-xs text-neutral-500" {
"Off is the safe state and the kill switch. With this unchecked every new \
call uses the voice servers and a running peer-to-peer call accepts no \
new joins, so the rollout and targeting fields below have no effect at \
all. While it is on, the fields below select who can start a \
peer-to-peer call, and anyone who agrees to a direct connection can \
join one."
}
h3 class="text-sm font-semibold text-neutral-900" { "Call size" }
(number_field(
"voice_p2p_max_participants",
"Maximum participants",
&voice_p2p.max_participants.to_string(),
Some(*VOICE_P2P_MAX_PARTICIPANTS_RANGE.start()),
Some(*VOICE_P2P_MAX_PARTICIPANTS_RANGE.end()),
"1",
Some("Most participants a peer-to-peer call can hold, 2 through 4. Clients only offer a peer-to-peer call in a direct message or group with at most this many members, and a join past it is refused as full. Lowering it never removes anyone from a running call."),
))
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
(number_field(
"voice_p2p_rollout_basis_points",
"Rollout (basis points)",
&voice_p2p.rollout_basis_points.to_string(),
Some(0), Some(10000), "1",
Some("Share of users who can start peer-to-peer calls, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."),
))
(experiment_rollout_country_codes_field(
"voice_p2p_rollout_country_codes",
&voice_p2p.rollout_country_codes,
))
div class="flex flex-col gap-2" {
(text_input(
"voice_p2p_rollout_salt",
"Rollout Salt",
&voice_p2p.rollout_salt,
VOICE_P2P_DEFAULT_SALT,
))
p class="text-xs text-neutral-500" {
"Seeds the bucketing hash. Changing it reshuffles which users fall \
inside the percentage above. Leave it alone to keep the current \
cohort stable."
}
}
div class="flex flex-col gap-2" {
(textarea_input(
"voice_p2p_included_user_ids",
"Always-on User IDs",
"1500000000000000001\n1500000000000000002",
&included_user_ids,
4,
false,
))
(entry_count_hint(
voice_p2p.included_user_ids.len(),
EXPERIMENT_MAX_TARGETED_USERS,
))
p class="text-xs text-neutral-500" {
"One snowflake per line, or comma separated. These users are targeted \
regardless of the percentage above. IDs must contain 1 to 20 decimal \
digits. Invalid entries prevent the save. Blank entries and duplicate \
IDs are ignored."
}
}
div class="flex flex-col gap-2" {
(checkbox(
"voice_p2p_include_premium_users",
"true",
"Include premium users",
voice_p2p.include_premium_users,
true,
))
p class="text-xs text-neutral-500" {
"Includes every account with active premium perks, regardless of the \
percentage above. The never-on list still wins."
}
}
div class="flex flex-col gap-2" {
(textarea_input(
"voice_p2p_included_guild_ids",
"Always-on Guild IDs",
"1500000000000000005\n1500000000000000006",
&voice_p2p.included_guild_ids.join("\n"),
4,
false,
))
(entry_count_hint(
voice_p2p.included_guild_ids.len(),
EXPERIMENT_MAX_TARGETED_USERS,
))
p class="text-xs text-neutral-500" {
"Same format, with guild IDs. Every member of a listed guild is \
included regardless of the percentage above, unless the user is \
in the never-on list."
}
}
div class="flex flex-col gap-2" {
(textarea_input(
"voice_p2p_excluded_user_ids",
"Never-on User IDs",
"1500000000000000003\n1500000000000000004",
&excluded_user_ids,
4,
false,
))
(entry_count_hint(
voice_p2p.excluded_user_ids.len(),
EXPERIMENT_MAX_TARGETED_USERS,
))
p class="text-xs text-neutral-500" {
"Same format. Exclusion wins over both the always-on list and the \
percentage."
}
}
(form_actions(html! {
(submit_button("Save Peer-to-Peer Voice Configuration"))
}))
}
}
},
)
}
fn estimate_low_end_solve_seconds(cost: u32, max_counter: u32) -> f64 {
0.75 * f64::from(cost) * f64::from(max_counter) / 1_050_000.0
}
fn captcha_section(base: &str, csrf_token: &str, captcha: &CaptchaConfigResponse) -> Markup {
let status = if captcha.enabled {
("On", BadgeVariant::Success)
} else {
("Off", BadgeVariant::Default)
};
let estimate = estimate_low_end_solve_seconds(captcha.cost, captcha.max_counter);
section_card_with_description(
"Proof-of-work check",
"Clients solve it in the background. The API issues and verifies every challenge itself, \
and no third party is involved.",
html! {
div class="space-y-6" {
div class="flex flex-wrap items-center gap-2" {
(badge(status.0, status.1))
}
form method="post" action={(base) "/instance-config?action=update_captcha"} {
(csrf_input(csrf_token))
div class="space-y-6" {
(checkbox(
"captcha_enabled",
"true",
"Require a proof-of-work check",
captcha.enabled,
true,
))
p class="text-xs text-neutral-500" {
"On by default. Turning it off removes the check from every request."
}
(number_field(
"captcha_cost",
"Cost (PBKDF2 iterations per try)",
&captcha.cost.to_string(),
Some(*CAPTCHA_COST_RANGE.start()),
Some(*CAPTCHA_COST_RANGE.end()),
"1",
Some("Default 5000."),
))
(number_field(
"captcha_max_counter",
"Maximum counter",
&captcha.max_counter.to_string(),
Some(*CAPTCHA_MAX_COUNTER_RANGE.start()),
Some(*CAPTCHA_MAX_COUNTER_RANGE.end()),
"1",
Some("Default 1000. Solve time grows with cost times this value."),
))
p class="text-sm text-neutral-700" {
(format!(
"Average solve: about {estimate:.1} s on a low-end Android device, \
well under a second in desktop browsers."
))
}
(form_actions(html! {
(submit_button("Save"))
}))
}
}
}
},
)
}
fn experiment_delivery_section(
base: &str,
csrf_token: &str,
experiment_delivery: &ExperimentDeliveryConfigResponse,
) -> Markup {
section_card_with_description(
"Experiment Delivery",
"How often every client revalidates its experiment assignments. This is instance-wide \
and covers every experiment, not just the ones above. Raising the interval sheds \
request volume and makes a change take longer to reach a client. Raising the jitter \
spreads a fleet that has synchronized on one tick back out across the interval.",
html! {
form method="post" action={(base) "/instance-config?action=update_experiment_delivery"} {
(csrf_input(csrf_token))
div class="space-y-6" {
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(number_field(
"experiment_delivery_poll_interval_seconds",
"Assignment Poll Interval (s)",
&experiment_delivery.poll_interval_seconds.to_string(),
Some(60), Some(86400), "1",
Some("How often a client re-reads its assignments, 60 to 86400 seconds. Lower values pick up changes sooner at the cost of more requests."),
))
(number_field(
"experiment_delivery_poll_jitter_percent",
"Assignment Poll Jitter (%)",
&experiment_delivery.poll_jitter_percent.to_string(),
Some(0), Some(50), "1",
Some("How far each client spreads its poll around the interval, 0 to 50 percent. Raise it to break up a fleet that polls on the same tick, set it to 0 for an exact interval."),
))
}
(form_actions(html! {
(submit_button("Save Experiment Delivery Configuration"))
}))
}
}
},
)
}
fn registration_config_section(
config: &AdminConfig,
csrf_token: &str,
registration: &InstanceRegistrationResponse,
collect_date_of_birth: bool,
self_hosted: bool,
) -> Markup {
let base = &config.base_path;
section_card_with_description(
"Registration Controls",
"Control public registration, admin-issued registration URLs, and pending approval requests.",
html! {
div class="space-y-8" {
(registration_policy_form(base, csrf_token, registration))
@if self_hosted {
(registration_fields_form(base, csrf_token, collect_date_of_birth))
}
(create_registration_url_form(base, csrf_token))
(registration_url_list(config, csrf_token, &registration.urls))
(pending_registration_list(config, csrf_token, &registration.pending_registrations))
}
},
)
}
fn registration_policy_form(
base: &str,
csrf_token: &str,
registration: &InstanceRegistrationResponse,
) -> Markup {
html! {
div class="space-y-4" {
h3 class="text-sm font-semibold text-neutral-900" { "Registration Policy" }
form method="post" action={(base) "/instance-config?action=update_registration"} {
(csrf_input(csrf_token))
div class="space-y-4" {
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(select_input("registration_mode", "Mode", &[
("open", "Open"),
("approval", "Approval required"),
("closed", "Closed"),
], registration.mode.as_str()))
div class="flex items-end pb-1" {
(checkbox(
"admin_registration_urls_enabled",
"true",
"Admin registration URLs enabled",
registration.admin_registration_urls_enabled,
true,
))
}
}
(form_actions(html! {
(submit_button("Save Registration Policy"))
}))
}
}
}
}
}
fn registration_fields_form(base: &str, csrf_token: &str, collect_date_of_birth: bool) -> Markup {
html! {
div class="space-y-4 border-t border-neutral-200 pt-6" {
h3 class="text-sm font-semibold text-neutral-900" { "Registration Fields" }
form method="post" action={(base) "/instance-config?action=update_app_registration"} {
(csrf_input(csrf_token))
div class="space-y-4" {
(checkbox(
"app_collect_date_of_birth",
"true",
"Collect date of birth during registration",
collect_date_of_birth,
true,
))
p class="text-xs text-neutral-500" {
"When disabled, new self-hosted accounts do not store a date of birth and are treated as adults."
}
(form_actions(html! {
(submit_button("Save Registration Fields"))
}))
}
}
}
}
}
fn create_registration_url_form(base: &str, csrf_token: &str) -> Markup {
let action = format!("{base}/instance-config?action=create_registration_url");
html! {
div class="space-y-4 border-t border-neutral-200 pt-6" {
h3 class="text-sm font-semibold text-neutral-900" { "Create Admin-Issued Registration URL" }
form method="post" action=(&action)
hx-post=(&action)
hx-target="#registration-url-list"
hx-swap="outerHTML"
hx-push-url="false"
data-admin-allow-swap="true" {
(csrf_input(csrf_token))
div class="space-y-4" {
div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4" {
(text_input(
"registration_url_label",
"Label",
"",
"Invite batch, partner, or support case",
))
(number_field(
"registration_url_expires_in_days",
"Expires In Days",
"",
Some(1),
None,
"1",
Some("Leave blank for no expiration."),
))
(number_field(
"registration_url_max_uses",
"Max Uses",
"",
Some(1),
Some(1_000_000),
"1",
Some("Leave blank for unlimited uses."),
))
div class="flex items-end pb-1" {
(checkbox(
"registration_url_approval_required",
"true",
"Approval required",
false,
true,
))
}
}
(form_actions(html! {
(submit_button("Create Registration URL"))
}))
}
}
}
}
}
pub fn registration_url_list(
config: &AdminConfig,
csrf_token: &str,
urls: &[RegistrationUrlResponse],
) -> Markup {
let base = &config.base_path;
html! {
div id="registration-url-list" class="space-y-4 border-t border-neutral-200 pt-6" {
h3 class="text-sm font-semibold text-neutral-900" { "Existing Registration URLs" }
@if urls.is_empty() {
p class="text-sm text-neutral-500" { "No admin-issued registration URLs." }
} @else {
div class="overflow-x-auto rounded-lg border border-neutral-200" {
table class="min-w-[1100px] divide-y divide-neutral-200 text-sm" {
thead class="bg-neutral-50" {
tr {
th scope="col" class="px-4 py-3 text-left font-semibold text-neutral-600" { "Label / URL" }
th scope="col" class="px-4 py-3 text-left font-semibold text-neutral-600" { "Created" }
th scope="col" class="px-4 py-3 text-left font-semibold text-neutral-600" { "Expires" }
th scope="col" class="px-4 py-3 text-left font-semibold text-neutral-600" { "Uses" }
th scope="col" class="px-4 py-3 text-left font-semibold text-neutral-600" { "Status" }
th scope="col" class="px-4 py-3 text-left font-semibold text-neutral-600" { "Approval" }
th scope="col" class="px-4 py-3 text-left font-semibold text-neutral-600" { "Action" }
}
}
tbody class="divide-y divide-neutral-200 bg-white" {
@for url in urls {
(registration_url_row(config, base, csrf_token, url))
}
}
}
}
}
}
}
}
fn registration_url_row(
config: &AdminConfig,
base: &str,
csrf_token: &str,
url: &RegistrationUrlResponse,
) -> Markup {
let label = url.label.as_deref().unwrap_or("Unlabeled");
let full_url = admin_issued_registration_url(config, &url.id);
let created_at = format_admin_timestamp(&url.created_at);
let expires_at = format_optional_admin_timestamp(url.expires_at.as_deref(), "Never");
let uses = registration_url_uses_text(url);
let status = registration_url_status(url);
let approval = if url.approval_required {
"Required"
} else {
"Not required"
};
let revoke_action = format!("{base}/instance-config?action=revoke_registration_url");
html! {
tr {
td class="min-w-[28rem] px-4 py-3 align-top" {
p class="font-medium text-neutral-900" { (label) }
(copyable_registration_url(&full_url, "Copy URL"))
p class="mt-1 whitespace-nowrap text-xs text-neutral-500" { "ID: " (&url.id) }
}
td class="px-4 py-3 align-top text-neutral-700" {
p class="whitespace-nowrap" { (created_at) }
p class="whitespace-nowrap text-xs text-neutral-500" { "By: " (&url.created_by_user_id) }
}
td class="px-4 py-3 align-top text-neutral-700 whitespace-nowrap" {
(expires_at)
}
td class="px-4 py-3 align-top text-neutral-700 whitespace-nowrap" {
(uses)
}
td class="px-4 py-3 align-top whitespace-nowrap" {
(registration_url_status_badge(status))
}
td class="px-4 py-3 align-top text-neutral-700 whitespace-nowrap" {
(approval)
}
td class="px-4 py-3 align-top" {
@if status == RegistrationUrlStatus::Active {
form method="post" action=(&revoke_action)
hx-post=(&revoke_action)
hx-target="#registration-url-list"
hx-swap="outerHTML"
hx-push-url="false"
data-admin-allow-swap="true" {
(csrf_input(csrf_token))
input type="hidden" name="registration_url_id" value=(&url.id);
(compact_button("Revoke", true))
}
} @else {
span class="text-sm text-neutral-400" { "No action" }
}
}
}
}
}
pub fn pending_registration_list(
config: &AdminConfig,
csrf_token: &str,
pending_registrations: &[PendingRegistrationResponse],
) -> Markup {
let base = &config.base_path;
html! {
div id="pending-registration-list" class="space-y-4 border-t border-neutral-200 pt-6" {
h3 class="text-sm font-semibold text-neutral-900" { "Pending Registrations" }
@if pending_registrations.is_empty() {
p class="text-sm text-neutral-500" { "No pending registrations." }
} @else {
div class="overflow-x-auto rounded-lg border border-neutral-200" {
table class="min-w-[940px] divide-y divide-neutral-200 text-sm" {
thead class="bg-neutral-50" {
tr {
th scope="col" class="px-4 py-3 text-left font-semibold text-neutral-600" { "Applicant" }
th scope="col" class="px-4 py-3 text-left font-semibold text-neutral-600" { "Requested" }
th scope="col" class="px-4 py-3 text-left font-semibold text-neutral-600" { "Registration URL" }
th scope="col" class="px-4 py-3 text-left font-semibold text-neutral-600" { "IP" }
th scope="col" class="px-4 py-3 text-left font-semibold text-neutral-600" { "Action" }
}
}
tbody class="divide-y divide-neutral-200 bg-white" {
@for pending in pending_registrations {
(pending_registration_row(config, base, csrf_token, pending))
}
}
}
}
}
}
}
}
fn pending_registration_row(
config: &AdminConfig,
base: &str,
csrf_token: &str,
pending: &PendingRegistrationResponse,
) -> Markup {
let account_name = pending_registration_account_name(pending);
let requested_at = format_admin_timestamp(&pending.requested_at);
let email = pending.email.as_deref().unwrap_or("None");
let link_id = pending.registration_url_id.as_deref().unwrap_or("None");
let client_ip = pending.client_ip.as_deref().unwrap_or("None");
let approve_action = format!("{base}/instance-config?action=approve_pending_registration");
let reject_action = format!("{base}/instance-config?action=reject_pending_registration");
html! {
tr {
td class="max-w-[20rem] px-4 py-3 align-top" {
p class="font-medium text-neutral-900" { (account_name) }
@if let Some(global_name) = pending.global_name.as_deref() {
p class="truncate text-xs text-neutral-500" title=(global_name) { (global_name) }
}
p class="truncate text-xs text-neutral-500" title=(email) { (email) }
p class="whitespace-nowrap text-xs text-neutral-500" { "ID: " (&pending.user_id) }
}
td class="px-4 py-3 align-top text-neutral-700 whitespace-nowrap" {
(requested_at)
}
td class="px-4 py-3 align-top text-neutral-700" {
@if let Some(registration_url_id) = pending.registration_url_id.as_deref() {
@let full_url = admin_issued_registration_url(config, registration_url_id);
p class="whitespace-nowrap text-xs text-neutral-500" { "ID: " (registration_url_id) }
div class="mt-1" {
(compact_copy_button(&full_url, "Copy URL"))
}
} @else {
span class="text-neutral-500" { (link_id) }
}
}
td class="px-4 py-3 align-top text-neutral-700 whitespace-nowrap" {
(client_ip)
}
td class="px-4 py-3 align-top" {
div class="flex flex-nowrap gap-2" {
form method="post" action=(&approve_action)
hx-post=(&approve_action)
hx-target="#pending-registration-list"
hx-swap="outerHTML"
hx-push-url="false"
data-admin-allow-swap="true" {
(csrf_input(csrf_token))
input type="hidden" name="user_id" value=(&pending.user_id);
(compact_button("Approve", false))
}
form method="post" action=(&reject_action)
hx-post=(&reject_action)
hx-target="#pending-registration-list"
hx-swap="outerHTML"
hx-push-url="false"
data-admin-allow-swap="true" {
(csrf_input(csrf_token))
input type="hidden" name="user_id" value=(&pending.user_id);
(compact_button("Reject", true))
}
}
}
}
}
}
fn admin_issued_registration_url(config: &AdminConfig, code: &str) -> String {
format!(
"{}/register?registration_url={}",
config.web_app_endpoint.trim_end_matches('/'),
urlencoding::encode(code)
)
}
fn copyable_registration_url(value: &str, label: &str) -> Markup {
html! {
div class="mt-2 flex items-center gap-2" {
input type="url" readonly value=(value)
aria-label="Registration URL"
class="h-8 min-w-0 flex-1 rounded-lg border border-neutral-300 bg-neutral-50 px-3 py-1.5 text-xs text-neutral-800";
(compact_copy_button(value, label))
}
}
}
fn compact_copy_button(value: &str, label: &str) -> Markup {
html! {
button type="button"
class="inline-flex h-8 shrink-0 items-center justify-center rounded-lg border border-neutral-300 bg-neutral-50 px-3 text-xs font-medium text-neutral-700 transition-all hover:border-neutral-400 hover:text-neutral-900 focus:outline-none focus:ring-2 focus:ring-brand-primary/20"
data-copy-value=(value)
onclick="window.__adminCopyToClipboard && window.__adminCopyToClipboard(this.dataset.copyValue, this, 'Copied')" {
(label)
}
}
}
fn compact_button(label: &str, danger: bool) -> Markup {
let variant_class = if danger {
"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500/30"
} else {
"bg-neutral-900 text-white hover:bg-neutral-800 focus:ring-brand-primary/20"
};
html! {
button type="submit"
class={"inline-flex h-8 shrink-0 items-center justify-center rounded-lg px-3 text-xs font-medium transition-all focus:outline-none focus:ring-2 " (variant_class)} {
(label)
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum RegistrationUrlStatus {
Active,
Revoked,
Expired,
Exhausted,
}
impl RegistrationUrlStatus {
fn label(self) -> &'static str {
match self {
Self::Active => "Active",
Self::Revoked => "Revoked",
Self::Expired => "Expired",
Self::Exhausted => "Exhausted",
}
}
fn variant(self) -> BadgeVariant {
match self {
Self::Active => BadgeVariant::Success,
Self::Revoked => BadgeVariant::Danger,
Self::Expired | Self::Exhausted => BadgeVariant::Warning,
}
}
}
fn registration_url_status(url: &RegistrationUrlResponse) -> RegistrationUrlStatus {
if url.revoked_at.is_some() {
return RegistrationUrlStatus::Revoked;
}
if url.expires_at.as_deref().is_some_and(timestamp_is_past) {
return RegistrationUrlStatus::Expired;
}
if url
.max_uses
.is_some_and(|max_uses| url.use_count >= max_uses)
{
return RegistrationUrlStatus::Exhausted;
}
RegistrationUrlStatus::Active
}
fn registration_url_status_badge(status: RegistrationUrlStatus) -> Markup {
badge(status.label(), status.variant())
}
fn timestamp_is_past(iso: &str) -> bool {
time::OffsetDateTime::parse(iso, &time::format_description::well_known::Rfc3339)
.or_else(|_| {
time::OffsetDateTime::parse(
iso,
&time::format_description::well_known::Iso8601::DEFAULT,
)
})
.map(|timestamp| timestamp < time::OffsetDateTime::now_utc())
.unwrap_or(false)
}
fn format_optional_admin_timestamp(value: Option<&str>, fallback: &str) -> String {
value
.map(format_admin_timestamp)
.unwrap_or_else(|| fallback.to_owned())
}
fn registration_url_uses_text(url: &RegistrationUrlResponse) -> String {
match url.max_uses {
Some(max_uses) => format!("{} / {}", url.use_count, max_uses),
None => format!("{} / unlimited", url.use_count),
}
}
fn pending_registration_account_name(pending: &PendingRegistrationResponse) -> String {
if pending.discriminator == 0 {
pending.username.clone()
} else {
format!("{}#{:04}", pending.username, pending.discriminator)
}
}
fn sso_config_section(base: &str, csrf_token: &str, sso: &SsoConfigResponse) -> Markup {
let allowed_domains = sso.allowed_domains.join("\n");
let secret_caption = if sso.client_secret_set {
"A secret is set. Check to clear, or enter a new value to rotate."
} else {
"No secret set yet."
};
section_card_with_description(
"Single Sign-On (SSO)",
"Configure OIDC-style SSO for the admin and client apps. When enabled, \
users can sign in through your SSO provider. Require SSO to block local \
password registration and login.",
html! {
form method="post" action={(base) "/instance-config?action=update_sso"} {
(csrf_input(csrf_token))
div class="space-y-6" {
div class="flex flex-col gap-2" {
(checkbox("sso_enabled", "true", "Enable SSO", sso.enabled, true))
(checkbox("sso_enforced", "true", "Require SSO (disables local password login and registration)", sso.enforced, true))
(checkbox("sso_auto_provision", "true", "Automatically provision users on first SSO login", sso.auto_provision, true))
}
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(text_input("sso_display_name", "Display Name", sso.display_name.as_deref().unwrap_or(""), "Example Identity Provider"))
(text_input("sso_issuer", "Issuer", sso.issuer.as_deref().unwrap_or(""), "https://idp.example.com"))
}
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(text_input("sso_authorization_url", "Authorization URL", sso.authorization_url.as_deref().unwrap_or(""), "https://idp.example.com/oauth/authorize"))
(text_input("sso_token_url", "Token URL", sso.token_url.as_deref().unwrap_or(""), "https://idp.example.com/oauth/token"))
}
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(text_input("sso_userinfo_url", "User Info URL", sso.userinfo_url.as_deref().unwrap_or(""), "https://idp.example.com/oauth/userinfo"))
(text_input("sso_jwks_url", "JWKS URL", sso.jwks_url.as_deref().unwrap_or(""), "https://idp.example.com/.well-known/jwks.json"))
}
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(text_input("sso_client_id", "Client ID", sso.client_id.as_deref().unwrap_or(""), "client-id"))
div class="space-y-2" {
(form_field_group("Client Secret", "sso_client_secret", false, None, None, html! {
input type="password" id="sso_client_secret" name="sso_client_secret"
placeholder="Leave blank to keep existing"
class=(FORM_INPUT_CLASS);
}))
(checkbox("sso_clear_client_secret", "true", "Clear secret", false, true))
p class="text-xs text-neutral-500" {
(secret_caption)
}
}
}
div class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
(text_input("sso_scope", "Scope", sso.scope.as_deref().unwrap_or(""), "openid email profile"))
(form_field_group("Redirect URI", "sso_redirect_uri", false, None,
Some("Configure this exact URI in your IdP. It is derived from the public gateway URL."),
html! {
input type="url" id="sso_redirect_uri" name="sso_redirect_uri"
value=(sso.redirect_uri.as_deref().unwrap_or(""))
disabled class=(FORM_INPUT_CLASS);
},
))
}
div class="space-y-2" {
(textarea_input(
"sso_allowed_domains",
"Allowed Email Domains",
"example.com\nexample.org",
&allowed_domains,
3,
false,
))
p class="text-xs text-neutral-500" {
"Limit SSO logins to these domains (one per line). Leave empty to allow any verified email."
}
}
(form_actions(html! {
(submit_button("Save SSO Settings"))
}))
}
}
},
)
}
fn limit_config_section(base: &str, limit_config: &LimitConfigResponse) -> Markup {
let description = if limit_config.self_hosted.unwrap_or(false) {
"Self-hosted instance with all premium features enabled by default. Configure user and guild limits."
} else {
"Configure limit rules that control user and guild restrictions based on traits and features."
};
section_card_simple(
"Limit Configuration",
html! {
div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between" {
div class="space-y-1" {
p class="text-sm text-neutral-500" {
(description)
}
}
(secondary_button_link("Configure Limits", &format!("{base}/limit-config")))
}
},
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn username_instances_hide_email_delivery_and_the_smtp_test() {
let integrations = InstanceIntegrationsResponse::default();
let username = integrations_config_section(
"/admin",
"csrf",
&integrations,
AccountIdentityMode::Username,
"Fluxer",
)
.into_string();
assert!(!username.contains("Email delivery"));
assert!(!username.contains("test_smtp"));
assert!(!username.contains("integration_email_present"));
assert!(username.contains("Bluesky OAuth"));
let email = integrations_config_section(
"/admin",
"csrf",
&integrations,
AccountIdentityMode::Email,
"Fluxer",
)
.into_string();
assert!(email.contains("Email delivery"));
assert!(email.contains("test_smtp"));
assert!(email.contains(r#"name="integration_email_present" value="1""#));
}
#[test]
fn account_identity_section_has_no_tag_choice_in_username_mode() {
let markup = account_identity_section(&AccountIdentityConfigResponse {
mode: AccountIdentityMode::Username,
locked: Some(true),
tag_style: TagStyle::None,
})
.into_string();
assert!(markup.contains("Sign-in Method"));
assert!(markup.contains("Username"));
assert!(markup.contains("Fixed"));
assert!(!markup.contains("No tags"));
assert!(!markup.contains("Random tags"));
assert!(!markup.contains("username tags"));
assert!(!markup.contains("<form"));
assert!(!markup.contains("<input"));
}
#[test]
fn account_identity_section_shows_random_tags_in_email_mode() {
let markup = account_identity_section(&AccountIdentityConfigResponse {
mode: AccountIdentityMode::Email,
locked: Some(true),
tag_style: TagStyle::Random,
})
.into_string();
assert!(markup.contains("Random tags"));
assert!(!markup.contains("No tags"));
assert!(markup.contains("username tags"));
}
#[test]
fn account_identity_section_shows_no_tags_in_email_mode() {
let markup = account_identity_section(&AccountIdentityConfigResponse {
mode: AccountIdentityMode::Email,
locked: Some(true),
tag_style: TagStyle::None,
})
.into_string();
assert!(markup.contains("No tags"));
assert!(!markup.contains("Random tags"));
assert!(!markup.contains("<input"));
}
#[test]
fn account_identity_section_shows_the_lock_state_only_when_known() {
let render = |locked| {
account_identity_section(&AccountIdentityConfigResponse {
mode: AccountIdentityMode::Email,
locked,
tag_style: TagStyle::Random,
})
.into_string()
};
let unlocked = render(Some(false));
assert!(unlocked.contains("Not fixed yet"));
let unknown = render(None);
assert!(!unknown.contains("Fixed"));
assert!(!unknown.contains("Not fixed yet"));
assert!(unknown.contains("Random tags"));
}
#[test]
fn captcha_section_posts_the_switch_and_difficulty_fields() {
let markup =
captcha_section("/admin", "csrf", &CaptchaConfigResponse::default()).into_string();
assert!(markup.contains("/admin/instance-config?action=update_captcha"));
assert!(markup.contains(r#"name="captcha_enabled""#));
assert!(markup.contains(r#"name="captcha_cost""#));
assert!(markup.contains(r#"name="captcha_max_counter""#));
assert!(markup.contains("about 3.6 s"));
}
#[test]
fn low_end_solve_estimate_at_the_defaults_is_about_three_and_a_half_seconds() {
let seconds = estimate_low_end_solve_seconds(5_000, 1_000);
assert!((seconds - 3.57).abs() < 0.01, "{seconds}");
}
#[test]
fn domain_migration_section_shows_both_rollouts_and_list_counts() {
let domain_migration = DomainMigrationConfigResponse {
anonymous_rollout_basis_points: 250,
included_user_ids: vec!["1500000000000000001".to_owned()],
excluded_user_ids: vec![
"1500000000000000002".to_owned(),
"1500000000000000003".to_owned(),
],
..DomainMigrationConfigResponse::default()
};
let markup = domain_migration_section("/admin", "csrf", &domain_migration).into_string();
assert!(markup.contains("action=update_domain_migration"));
assert!(markup.contains("domain_migration_enabled"));
assert!(markup.contains("name=\"domain_migration_anonymous_rollout_basis_points\""));
assert!(markup.contains("value=\"250\""));
assert!(markup.contains("name=\"domain_migration_standalone_forwarding\""));
assert!(markup.contains("name=\"domain_migration_rollout_country_codes\""));
assert!(markup.contains("1 of 1000 stored"));
assert!(markup.contains("2 of 1000 stored"));
assert!(!markup.contains("at the cap"));
}
#[test]
fn voice_p2p_section_shows_the_rollout_and_list_counts() {
let voice_p2p = VoiceP2pConfigResponse {
enabled: true,
config_version: 3,
rollout_basis_points: 250,
rollout_country_codes: vec!["SE".to_owned(), "NO".to_owned()],
max_participants: 3,
included_user_ids: vec!["1500000000000000001".to_owned()],
excluded_user_ids: vec![
"1500000000000000002".to_owned(),
"1500000000000000003".to_owned(),
],
..VoiceP2pConfigResponse::default()
};
let markup = voice_p2p_section("/admin", "csrf", &voice_p2p).into_string();
assert!(markup.contains("Peer-to-peer voice"));
assert!(markup.contains("action=update_voice_p2p"));
assert!(markup.contains("name=\"voice_p2p_enabled\""));
assert!(markup.contains("name=\"voice_p2p_rollout_basis_points\""));
assert!(markup.contains("value=\"250\""));
assert!(markup.contains("name=\"voice_p2p_max_participants\""));
assert!(markup.contains("value=\"3\""));
assert!(markup.contains("min=\"2\""));
assert!(markup.contains("max=\"4\""));
assert!(markup.contains("name=\"voice_p2p_include_premium_users\""));
assert!(markup.contains("name=\"voice_p2p_included_guild_ids\""));
assert!(markup.contains("name=\"voice_p2p_rollout_country_codes\""));
assert!(markup.contains("value=\"SE, NO\""));
assert!(markup.contains("2 of 250 stored"));
assert!(markup.contains("Config version 3"));
assert!(markup.contains("1 of 1000 stored"));
assert!(markup.contains("2 of 1000 stored"));
assert!(!markup.contains("anonymous_rollout_basis_points"));
assert!(!markup.contains("standalone_forwarding"));
}
#[test]
fn push_relay_section_shows_the_consent_toggle() {
let accepted = PushRelayConfigResponse {
relay_consent_accepted: true,
relay_consent_accepted_at: Some("2026-09-27T10:11:12.000Z".to_owned()),
relay_consent_accepted_by: Some("1130650140672000000".to_owned()),
};
let markup = push_relay_section("/admin", "csrf", &accepted).into_string();
assert!(markup.contains("action=update_push_relay"));
assert!(markup.contains("name=\"push_relay_consent_accepted\""));
assert!(markup.contains("https://fluxer.com/push-relay"));
assert!(markup.contains("value=\"Sep 27, 2026, 10:11 AM UTC\""));
assert!(markup.contains("value=\"1130650140672000000\""));
assert!(!markup.contains("name=\"push_relay_consent_accepted_at\""));
assert!(!markup.contains("name=\"push_relay_consent_accepted_by\""));
assert!(!markup.to_lowercase().contains("rollout"));
let unaccepted =
push_relay_section("/admin", "csrf", &PushRelayConfigResponse::default()).into_string();
assert!(unaccepted.contains("name=\"push_relay_consent_accepted\""));
assert!(unaccepted.contains("Not accepted"));
assert!(unaccepted.contains("value=\"Never\""));
assert!(unaccepted.contains("value=\"Nobody\""));
}
#[test]
fn premium_mode_options_use_the_configured_premium_name() {
let markup =
premium_mode_form("/admin", "csrf", &InstancePolicyResponse::default(), "Gold")
.into_string();
assert!(markup.contains("Mirror (Free and Gold tiers)"));
assert!(markup.contains("Everyone (every member gets Gold limits)"));
assert!(!markup.contains("Plutonium"));
}
#[test]
fn domain_migration_section_flags_a_list_at_its_cap() {
let domain_migration = DomainMigrationConfigResponse {
included_user_ids: (0..EXPERIMENT_MAX_TARGETED_USERS)
.map(|index| index.to_string())
.collect(),
..DomainMigrationConfigResponse::default()
};
let markup = domain_migration_section("/admin", "csrf", &domain_migration).into_string();
assert!(markup.contains("1000 of 1000 stored"));
assert!(markup.contains("at the cap"));
}
}