{% extends "base.twig" %}
{% block title %}{{ t('ui.admin.bbs_settings.page_title', {}, locale, ['common']) }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{{ t('ui.admin.bbs_settings.heading', {}, locale, ['common']) }}</h1>
</div>
<div class="row g-4 settings-loading" id="bbsSettingsCards">
<div class="col-lg-6">
<div class="card shadow h-100">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.admin.bbs_settings.system.title', {}, locale, ['common']) }}</h6>
</div>
<div class="card-body">
<div id="bbsSystemAlert"></div>
<form id="bbsSystemForm">
<div class="mb-3">
<label class="form-label" for="bbsSystemName">{{ t('ui.admin.bbs_settings.system.system_name', {}, locale, ['common']) }}</label>
<input type="text" class="form-control" id="bbsSystemName">
</div>
<div class="mb-3">
<label class="form-label" for="bbsSystemSysop">{{ t('ui.admin.bbs_settings.system.sysop_name', {}, locale, ['common']) }}</label>
<select class="form-select" id="bbsSystemSysop"></select>
<small class="text-muted">{{ t('ui.admin.bbs_settings.system.select_admin_user', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsSystemLocation">{{ t('ui.admin.bbs_settings.system.location', {}, locale, ['common']) }}</label>
<input type="text" class="form-control" id="bbsSystemLocation">
</div>
<div class="mb-3">
<label class="form-label" for="bbsSystemTimezone">{{ t('ui.admin.bbs_settings.system.timezone', {}, locale, ['common']) }}</label>
<select class="form-select" id="bbsSystemTimezone">
{% for tz in timezone_list %}
<option value="{{ tz }}">{{ tz }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label" for="bbsSystemAddress">{{ t('ui.admin.bbs_settings.system.system_address', {}, locale, ['common']) }}</label>
<input type="text" class="form-control" id="bbsSystemAddress">
</div>
<div class="mb-3">
<label class="form-label" for="bbsSystemOrigin">{{ t('ui.admin.bbs_settings.system.origin_line', {}, locale, ['common']) }}</label>
<input type="text" class="form-control" id="bbsSystemOrigin">
</div>
<button type="button" class="btn btn-primary" onclick="saveBbsSystem()">{{ t('ui.admin.bbs_settings.system.save', {}, locale, ['common']) }}</button>
</form>
<hr>
<h6 class="mb-3">{{ t('ui.admin.bbs_settings.terminal_server.title', {}, locale, ['common']) }}</h6>
<div id="bbsTerminalIdleAlert"></div>
<form id="bbsTerminalIdleForm">
<div class="mb-3">
<label class="form-label" for="bbsTerminalDefaultShell">{{ t('ui.admin.bbs_settings.terminal_server.default_shell', {}, locale, ['common']) }}</label>
<select class="form-select" id="bbsTerminalDefaultShell">
{% for shell in allowed_terminal_shells %}
<option value="{{ shell.id }}">
{% if shell.admin_label_key %}
{{ t(shell.admin_label_key, {}, locale, ['common']) }}
{% else %}
{{ shell.admin_label }}
{% endif %}
</option>
{% endfor %}
</select>
<small class="text-muted">{{ t('ui.admin.bbs_settings.terminal_server.default_shell_help', {}, locale, ['common']) }}</small>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="bbsTerminalForceShell">
<label class="form-check-label" for="bbsTerminalForceShell">{{ t('ui.admin.bbs_settings.terminal_server.force_shell', {}, locale, ['common']) }}</label>
<div><small class="text-muted">{{ t('ui.admin.bbs_settings.terminal_server.force_shell_help', {}, locale, ['common']) }}</small></div>
</div>
<div class="mb-3">
<label class="form-label" for="bbsIdleWarnMinutes">{{ t('ui.admin.bbs_settings.terminal_idle.warn_minutes', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsIdleWarnMinutes" min="1" step="1" value="5">
<small class="text-muted">{{ t('ui.admin.bbs_settings.terminal_idle.warn_minutes_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsIdleDisconnectMinutes">{{ t('ui.admin.bbs_settings.terminal_idle.disconnect_minutes', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsIdleDisconnectMinutes" min="2" step="1" value="7">
<small class="text-muted">{{ t('ui.admin.bbs_settings.terminal_idle.disconnect_minutes_help', {}, locale, ['common']) }}</small>
</div>
<button type="button" class="btn btn-primary" onclick="saveTerminalIdle()">{{ t('ui.admin.bbs_settings.terminal_server.save', {}, locale, ['common']) }}</button>
</form>
<hr>
<h6 class="mb-3">{{ t('ui.admin.bbs_settings.packet_bbs.title', {}, locale, ['common']) }}</h6>
<div id="bbsPacketBbsAlert"></div>
<form id="bbsPacketBbsForm">
<div class="mb-3">
<label class="form-label" for="bbsPacketBbsSessionTimeout">{{ t('ui.admin.bbs_settings.packet_bbs.session_timeout_minutes', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsPacketBbsSessionTimeout" min="1" step="1" value="15">
<small class="text-muted">{{ t('ui.admin.bbs_settings.packet_bbs.session_timeout_help', {}, locale, ['common']) }}</small>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="bbsPacketBbsAllowGuestWho">
<label class="form-check-label" for="bbsPacketBbsAllowGuestWho">{{ t('ui.admin.bbs_settings.packet_bbs.allow_guest_who', {}, locale, ['common']) }}</label>
<div><small class="text-muted">{{ t('ui.admin.bbs_settings.packet_bbs.allow_guest_who_help', {}, locale, ['common']) }}</small></div>
</div>
<button type="button" class="btn btn-primary" onclick="saveBbsPacketBbs()">{{ t('ui.admin.bbs_settings.packet_bbs.save', {}, locale, ['common']) }}</button>
</form>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card shadow h-100">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.admin.bbs_settings.features.title', {}, locale, ['common']) }}</h6>
</div>
<div class="card-body">
<div id="bbsSettingsAlert"></div>
<form id="bbsSettingsForm">
<div class="form-check mb-2 d-flex align-items-center gap-2">
<input class="form-check-input" type="checkbox" id="bbsWebdoors">
<label class="form-check-label" for="bbsWebdoors">{{ t('ui.admin.bbs_settings.features.enable_webdoors', {}, locale, ['common']) }}</label>
{% if not webdoors_active %}
<small class="text-muted">
({{ t('ui.admin.bbs_settings.features.inactive', {}, locale, ['common']) }})
<a href="/admin/webdoors" class="ms-1">{{ t('ui.admin.bbs_settings.features.activate', {}, locale, ['common']) }}</a>
</small>
{% endif %}
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="bbsShoutbox">
<label class="form-check-label" for="bbsShoutbox">{{ t('ui.admin.bbs_settings.features.enable_shoutbox', {}, locale, ['common']) }}</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="bbsAdvertising">
<label class="form-check-label" for="bbsAdvertising">{{ t('ui.admin.bbs_settings.features.enable_advertising', {}, locale, ['common']) }}</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="bbsVoting">
<label class="form-check-label" for="bbsVoting">{{ t('ui.admin.bbs_settings.features.enable_voting_booth', {}, locale, ['common']) }}</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="bbsChat">
<label class="form-check-label" for="bbsChat">{{ t('ui.admin.bbs_settings.features.enable_chat', {}, locale, ['common']) }}</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="bbsFileAreas">
<label class="form-check-label" for="bbsFileAreas">{{ t('ui.admin.bbs_settings.features.enable_file_areas', {}, locale, ['common']) }}</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="bbsGuestDoorsPage">
<label class="form-check-label" for="bbsGuestDoorsPage">{{ t('ui.admin.bbs_settings.features.enable_guest_doors_page', {}, locale, ['common']) }}</label>
<div><small class="text-muted">{{ t('ui.admin.bbs_settings.features.guest_doors_page_help', {}, locale, ['common']) }}</small></div>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="bbsBbsDirectory">
<label class="form-check-label" for="bbsBbsDirectory">{{ t('ui.admin.bbs_settings.features.enable_bbs_directory', {}, locale, ['common']) }}</label>
<div><small class="text-muted">{{ t('ui.admin.bbs_settings.features.bbs_directory_help', {}, locale, ['common']) }}</small></div>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="bbsQwk">
<label class="form-check-label" for="bbsQwk">{{ t('ui.admin.bbs_settings.features.enable_qwk', {}, locale, ['common']) }}</label>
<div><small class="text-muted">{{ t('ui.admin.bbs_settings.features.qwk_help', {}, locale, ['common']) }}</small></div>
</div>
<div class="mb-3">
<label class="form-label" for="bbsQwkBbsId">{{ t('ui.admin.bbs_settings.features.qwk_bbs_id', {}, locale, ['common']) }}</label>
<input type="text" class="form-control font-monospace" id="bbsQwkBbsId" maxlength="8" style="max-width:12rem;" placeholder="e.g. MYBBS">
<small class="text-muted">{{ t('ui.admin.bbs_settings.features.qwk_bbs_id_help', {}, locale, ['common']) }}</small>
</div>
{% if license_valid %}
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="bbsPublicFilesIndex">
<label class="form-check-label" for="bbsPublicFilesIndex">{{ t('ui.admin.bbs_settings.features.enable_public_files_index', {}, locale, ['common']) }}</label>
<div><small class="text-muted">{{ t('ui.admin.bbs_settings.features.public_files_index_help', {}, locale, ['common']) }}</small></div>
</div>
{% else %}
<div class="alert alert-secondary py-2 small mb-3">
<i class="fas fa-lock me-1"></i>
{{ t('ui.admin.bbs_settings.features.public_files_index_requires_license', {}, locale, ['common']) }}
</div>
{% endif %}
<div class="mb-3">
<label class="form-label" for="bbsDefaultEchoInterface">{{ t('ui.admin.bbs_settings.features.default_echo_interface', {}, locale, ['common']) }}</label>
<select class="form-select" id="bbsDefaultEchoInterface">
<option value="echolist">{{ t('ui.admin.bbs_settings.features.echo_list_forum', {}, locale, ['common']) }}</option>
<option value="reader">{{ t('ui.admin.bbs_settings.features.reader_message_list', {}, locale, ['common']) }}</option>
</select>
<small class="text-muted">{{ t('ui.admin.bbs_settings.features.default_echo_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsBulletinDisplayMode">{{ t('ui.admin.bbs_settings.features.bulletin_display_mode', {}, locale, ['common']) }}</label>
<select class="form-select" id="bbsBulletinDisplayMode">
<option value="once">{{ t('ui.admin.bbs_settings.features.bulletin_display_once', {}, locale, ['common']) }}</option>
<option value="always">{{ t('ui.admin.bbs_settings.features.bulletin_display_always', {}, locale, ['common']) }}</option>
</select>
<small class="text-muted">{{ t('ui.admin.bbs_settings.features.bulletin_display_help', {}, locale, ['common']) }}</small>
</div>
<hr>
<div class="mb-3">
<label class="form-label" for="bbsMaxCrossPost">{{ t('ui.admin.bbs_settings.features.max_cross_post_areas', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsMaxCrossPost" min="2" max="20" step="1" value="5">
<small class="text-muted">{{ t('ui.admin.bbs_settings.features.max_cross_post_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsEchomailModerationThreshold">{{ t('ui.admin.bbs_settings.features.echomail_moderation_threshold', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsEchomailModerationThreshold" min="0" step="1" value="0">
<small class="text-muted">{{ t('ui.admin.bbs_settings.features.echomail_moderation_threshold_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsDashboardAdRotateInterval">{{ t('ui.admin.bbs_settings.features.dashboard_ad_rotate_interval', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsDashboardAdRotateInterval" min="5" max="300" step="1" value="20">
<small class="text-muted">{{ t('ui.admin.bbs_settings.features.dashboard_ad_rotate_interval_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsOutgoingCharset">{{ t('ui.admin.bbs_settings.features.outgoing_charset', {}, locale, ['common']) }}</label>
<select class="form-select" id="bbsOutgoingCharset">
{% for cs in supported_charsets %}
<option value="{{ cs.value }}">{{ cs.label }}</option>
{% endfor %}
</select>
<small class="text-muted">{{ t('ui.admin.bbs_settings.features.outgoing_charset_help', {}, locale, ['common']) }}</small>
</div>
<button type="button" class="btn btn-primary" onclick="saveBbsSettings()">{{ t('ui.admin.bbs_settings.features.save', {}, locale, ['common']) }}</button>
</form>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.admin.bbs_settings.credits.title', {}, locale, ['common']) }}</h6>
</div>
<div class="card-body">
<div id="bbsCreditsAlert"></div>
<div class="row g-4">
<div class="col-md-6 col-xl-3">
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" id="bbsCreditsEnabled">
<label class="form-check-label" for="bbsCreditsEnabled">{{ t('ui.admin.bbs_settings.credits.enabled', {}, locale, ['common']) }}</label>
</div>
</div>
<div class="col-md-6 col-xl-3">
<div class="mb-3">
<label class="form-label" for="bbsCreditsSymbol">{{ t('ui.admin.bbs_settings.credits.currency_symbol', {}, locale, ['common']) }}</label>
<input type="text" class="form-control" id="bbsCreditsSymbol" maxlength="5">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.currency_symbol_help', {}, locale, ['common']) }}</small>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-lg-6">
<h6 class="mb-3">{{ t('ui.admin.bbs_settings.credits.costs_and_fees', {}, locale, ['common']) }}</h6>
<div class="mb-3">
<label class="form-label" for="bbsCreditsNetmailCost">{{ t('ui.admin.bbs_settings.credits.netmail_cost', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsCreditsNetmailCost" min="0" step="1">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.netmail_cost_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsCreditsCrashmailCost">{{ t('ui.admin.bbs_settings.credits.crashmail_cost', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsCreditsCrashmailCost" min="0" step="1">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.crashmail_cost_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsCreditsPollCreationCost">{{ t('ui.admin.bbs_settings.credits.poll_creation_cost', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsCreditsPollCreationCost" min="0" step="1">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.poll_creation_cost_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsCreditsFileUploadCost">{{ t('ui.admin.bbs_settings.credits.file_upload_cost', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsCreditsFileUploadCost" min="0" step="1">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.file_upload_cost_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsCreditsFileDownloadCost">{{ t('ui.admin.bbs_settings.credits.file_download_cost', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsCreditsFileDownloadCost" min="0" step="1">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.file_download_cost_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsCreditsAiPerMilliUsd">{{ t('ui.admin.bbs_settings.credits.ai_credits_per_milli_usd', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsCreditsAiPerMilliUsd" min="0" step="1">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.ai_credits_per_milli_usd_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsCreditsTransferFee">{{ t('ui.admin.bbs_settings.credits.transfer_fee_percentage', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsCreditsTransferFee" min="0" max="1" step="0.01">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.transfer_fee_help', {}, locale, ['common']) }}</small>
</div>
</div>
<div class="col-lg-6">
<h6 class="mb-3">{{ t('ui.admin.bbs_settings.credits.rewards_and_bonuses', {}, locale, ['common']) }}</h6>
<div class="mb-3">
<label class="form-label" for="bbsCreditsDailyAmount">{{ t('ui.admin.bbs_settings.credits.daily_login_bonus_amount', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsCreditsDailyAmount" min="0" step="1">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.daily_login_bonus_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsCreditsDailyDelay">{{ t('ui.admin.bbs_settings.credits.daily_login_delay_minutes', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsCreditsDailyDelay" min="0" step="1">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.daily_login_delay_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsCreditsApprovalBonus">{{ t('ui.admin.bbs_settings.credits.new_user_approval_bonus', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsCreditsApprovalBonus" min="0" step="1">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.new_user_approval_bonus_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsCreditsNew14Days">{{ t('ui.admin.bbs_settings.credits.new_user_14_day_bonus', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsCreditsNew14Days" min="0" step="1">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.new_user_14_day_bonus_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsCreditsEchomailReward">{{ t('ui.admin.bbs_settings.credits.echomail_reward', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsCreditsEchomailReward" min="0" step="1">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.echomail_reward_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsCreditsFileUploadReward">{{ t('ui.admin.bbs_settings.credits.file_upload_reward', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsCreditsFileUploadReward" min="0" step="1">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.file_upload_reward_help', {}, locale, ['common']) }}</small>
</div>
<div class="mb-3">
<label class="form-label" for="bbsCreditsFileDownloadReward">{{ t('ui.admin.bbs_settings.credits.file_download_reward', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsCreditsFileDownloadReward" min="0" step="1">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.file_download_reward_help', {}, locale, ['common']) }}</small>
</div>
<hr>
<h6 class="mb-3">{{ t('ui.admin.bbs_settings.credits.referral_system', {}, locale, ['common']) }}</h6>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" id="bbsCreditsReferralEnabled">
<label class="form-check-label" for="bbsCreditsReferralEnabled">{{ t('ui.admin.bbs_settings.credits.enable_referral_system', {}, locale, ['common']) }}</label>
</div>
<div class="mb-3">
<label class="form-label" for="bbsCreditsReferralBonus">{{ t('ui.admin.bbs_settings.credits.referral_bonus', {}, locale, ['common']) }}</label>
<input type="number" class="form-control" id="bbsCreditsReferralBonus" min="0" step="1">
<small class="text-muted">{{ t('ui.admin.bbs_settings.credits.referral_bonus_help', {}, locale, ['common']) }}</small>
</div>
</div>
</div>
<button type="button" class="btn btn-primary" onclick="saveBbsCredits()">{{ t('ui.admin.bbs_settings.credits.save', {}, locale, ['common']) }}</button>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.admin.bbs_settings.taglines.title', {}, locale, ['common']) }}</h6>
</div>
<div class="card-body">
<div id="bbsTaglinesAlert"></div>
<div class="mb-3">
<label class="form-label" for="bbsTaglinesText">{{ t('ui.admin.bbs_settings.taglines.one_per_line', {}, locale, ['common']) }}</label>
<textarea class="form-control" id="bbsTaglinesText" rows="8" placeholder="{{ t('ui.admin.bbs_settings.taglines.placeholder', {}, locale, ['common']) }}"></textarea>
<small class="text-muted">{{ t('ui.admin.bbs_settings.taglines.help', {}, locale, ['common']) }}</small>
</div>
<button type="button" class="btn btn-primary" onclick="saveBbsTaglines()">{{ t('ui.admin.bbs_settings.taglines.save', {}, locale, ['common']) }}</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function uiT(key, fallback, params = {}) {
if (window.t) {
return window.t(key, params, fallback);
}
return fallback;
}
document.addEventListener('DOMContentLoaded', function() {
setSettingsLoading('#bbsSettingsCards', true);
loadI18nNamespaces(['common', 'errors']).then(function() {
loadBbsSettings();
loadBbsSystem();
loadAdminUsers('bbsSystemSysop');
loadBbsTaglines();
});
});
function loadBbsSettings() {
fetch('/admin/api/bbs-settings')
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(apiError(data, uiT('ui.admin.bbs_settings.load_failed', 'Failed to load settings')));
}
const features = data.config?.features || {};
const credits = data.config?.credits || {};
document.getElementById('bbsWebdoors').checked = !!features.webdoors;
document.getElementById('bbsShoutbox').checked = !!features.shoutbox;
document.getElementById('bbsAdvertising').checked = !!features.advertising;
document.getElementById('bbsVoting').checked = !!features.voting_booth;
document.getElementById('bbsChat').checked = !!features.chat;
document.getElementById('bbsFileAreas').checked = !!features.file_areas;
document.getElementById('bbsGuestDoorsPage').checked = !!features.guest_doors_page;
document.getElementById('bbsBbsDirectory').checked = features.bbs_directory !== false;
document.getElementById('bbsQwk').checked = !!features.qwk;
document.getElementById('bbsQwkBbsId').value = data.config?.qwk?.bbs_id || '';
const bbsPublicFilesIndexEl = document.getElementById('bbsPublicFilesIndex');
if (bbsPublicFilesIndexEl) bbsPublicFilesIndexEl.checked = !!features.public_files_index;
document.getElementById('bbsDefaultEchoInterface').value = data.config?.default_echo_interface || 'echolist';
document.getElementById('bbsBulletinDisplayMode').value = data.config?.bulletin_display_mode === 'always' ? 'always' : 'once';
const maxCrossPost = parseInt(data.config?.max_cross_post_areas, 10);
document.getElementById('bbsMaxCrossPost').value = Number.isNaN(maxCrossPost) ? 5 : maxCrossPost;
const echomailModerationThreshold = parseInt(data.config?.echomail_moderation_threshold, 10);
document.getElementById('bbsEchomailModerationThreshold').value = Number.isNaN(echomailModerationThreshold) ? 0 : echomailModerationThreshold;
const dashboardAdRotateInterval = parseInt(data.config?.dashboard_ad_rotate_interval_seconds, 10);
document.getElementById('bbsDashboardAdRotateInterval').value = Number.isNaN(dashboardAdRotateInterval) ? 20 : dashboardAdRotateInterval;
document.getElementById('bbsOutgoingCharset').value = data.config?.outgoing_charset || 'CP437';
const terminalIdle = data.config?.terminal_idle || {};
const idleWarnMinutes = parseInt(terminalIdle.warn_minutes, 10);
const idleDisconnectMinutes = parseInt(terminalIdle.disconnect_minutes, 10);
document.getElementById('bbsIdleWarnMinutes').value = Number.isNaN(idleWarnMinutes) ? 5 : idleWarnMinutes;
document.getElementById('bbsIdleDisconnectMinutes').value = Number.isNaN(idleDisconnectMinutes) ? 7 : idleDisconnectMinutes;
const terminalServer = data.config?.terminal_server || {};
const terminalDefaultShellEl = document.getElementById('bbsTerminalDefaultShell');
const desiredDefaultShell = terminalServer.default_shell || 'tui';
const hasDesiredDefaultShell = Array.from(terminalDefaultShellEl.options).some(option => option.value === desiredDefaultShell);
terminalDefaultShellEl.value = hasDesiredDefaultShell
? desiredDefaultShell
: (terminalDefaultShellEl.options[0]?.value || 'tui');
document.getElementById('bbsTerminalForceShell').checked = !!terminalServer.force_shell;
const packetBbs = data.config?.packet_bbs || {};
const sessionTimeout = parseInt(packetBbs.session_timeout_minutes, 10);
document.getElementById('bbsPacketBbsSessionTimeout').value = Number.isNaN(sessionTimeout) ? 15 : sessionTimeout;
document.getElementById('bbsPacketBbsAllowGuestWho').checked = packetBbs.allow_guest_who !== false;
document.getElementById('bbsCreditsEnabled').checked = credits.enabled !== false;
document.getElementById('bbsCreditsSymbol').value = (credits.symbol ?? '$');
const dailyAmountValue = parseInt(credits.daily_amount, 10);
const dailyDelayValue = parseInt(credits.daily_login_delay_minutes, 10);
const approvalBonusValue = parseInt(credits.approval_bonus, 10);
const netmailCostValue = parseInt(credits.netmail_cost, 10);
const echomailRewardValue = parseInt(credits.echomail_reward, 10);
const crashmailCostValue = parseInt(credits.crashmail_cost, 10);
const pollCreationCostValue = parseInt(credits.poll_creation_cost, 10);
const fileUploadCostValue = parseInt(credits.file_upload_cost, 10);
const fileUploadRewardValue = parseInt(credits.file_upload_reward, 10);
const fileDownloadCostValue = parseInt(credits.file_download_cost, 10);
const fileDownloadRewardValue = parseInt(credits.file_download_reward, 10);
const aiPerMilliUsdValue = parseInt(credits.ai_credits_per_milli_usd, 10);
const new14DaysValue = parseInt(credits.return_14days, 10);
const transferFeeValue = parseFloat(credits.transfer_fee_percent);
document.getElementById('bbsCreditsDailyAmount').value = Number.isNaN(dailyAmountValue) ? 25 : dailyAmountValue;
document.getElementById('bbsCreditsDailyDelay').value = Number.isNaN(dailyDelayValue) ? 5 : dailyDelayValue;
document.getElementById('bbsCreditsApprovalBonus').value = Number.isNaN(approvalBonusValue) ? 100 : approvalBonusValue;
document.getElementById('bbsCreditsNetmailCost').value = Number.isNaN(netmailCostValue) ? 1 : netmailCostValue;
document.getElementById('bbsCreditsEchomailReward').value = Number.isNaN(echomailRewardValue) ? 3 : echomailRewardValue;
document.getElementById('bbsCreditsCrashmailCost').value = Number.isNaN(crashmailCostValue) ? 10 : crashmailCostValue;
document.getElementById('bbsCreditsPollCreationCost').value = Number.isNaN(pollCreationCostValue) ? 15 : pollCreationCostValue;
document.getElementById('bbsCreditsFileUploadCost').value = Number.isNaN(fileUploadCostValue) ? 0 : fileUploadCostValue;
document.getElementById('bbsCreditsFileUploadReward').value = Number.isNaN(fileUploadRewardValue) ? 0 : fileUploadRewardValue;
document.getElementById('bbsCreditsFileDownloadCost').value = Number.isNaN(fileDownloadCostValue) ? 0 : fileDownloadCostValue;
document.getElementById('bbsCreditsFileDownloadReward').value = Number.isNaN(fileDownloadRewardValue) ? 0 : fileDownloadRewardValue;
document.getElementById('bbsCreditsAiPerMilliUsd').value = Number.isNaN(aiPerMilliUsdValue) ? 0 : aiPerMilliUsdValue;
document.getElementById('bbsCreditsNew14Days').value = Number.isNaN(new14DaysValue) ? 50 : new14DaysValue;
document.getElementById('bbsCreditsTransferFee').value = Number.isNaN(transferFeeValue) ? 0.05 : transferFeeValue;
const referralBonusValue = parseInt(credits.referral_bonus, 10);
document.getElementById('bbsCreditsReferralEnabled').checked = !!credits.referral_enabled;
document.getElementById('bbsCreditsReferralBonus').value = Number.isNaN(referralBonusValue) ? 25 : referralBonusValue;
setSettingsLoading('#bbsSettingsCards', false);
})
.catch(error => {
showBbsSettingsAlert(error.message || uiT('ui.admin.bbs_settings.load_failed', 'Failed to load settings'), 'danger');
});
}
function saveBbsSettings() {
const maxCrossPost = parseInt(document.getElementById('bbsMaxCrossPost').value, 10);
if (Number.isNaN(maxCrossPost) || maxCrossPost < 2 || maxCrossPost > 20) {
showBbsSettingsAlert(uiT('ui.admin.bbs_settings.validation.max_cross_post_areas_range', 'Max cross-post areas must be an integer between 2 and 20.'), 'danger');
return;
}
const echomailModerationThreshold = parseInt(document.getElementById('bbsEchomailModerationThreshold').value, 10);
if (Number.isNaN(echomailModerationThreshold) || echomailModerationThreshold < 0) {
showBbsSettingsAlert(uiT('ui.admin.bbs_settings.validation.echomail_moderation_threshold_range', 'Echomail moderation threshold must be 0 or higher (0 = disabled).'), 'danger');
return;
}
const dashboardAdRotateInterval = parseInt(document.getElementById('bbsDashboardAdRotateInterval').value, 10);
if (Number.isNaN(dashboardAdRotateInterval) || dashboardAdRotateInterval < 5 || dashboardAdRotateInterval > 300) {
showBbsSettingsAlert(uiT('ui.admin.bbs_settings.validation.dashboard_ad_rotate_interval_range', 'Dashboard ad rotation interval must be an integer between 5 and 300 seconds.'), 'danger');
return;
}
const qwkBbsIdRaw = document.getElementById('bbsQwkBbsId').value.trim().toUpperCase().replace(/[^A-Z0-9]/g, '').substring(0, 8);
if (qwkBbsIdRaw.length === 0) {
showBbsSettingsAlert(uiT('ui.admin.bbs_settings.validation.qwk_bbs_id_invalid', 'QWK BBS ID must be 1?8 letters or digits.'), 'danger');
return;
}
document.getElementById('bbsQwkBbsId').value = qwkBbsIdRaw;
// Get current credits + qwk settings (don't change them)
fetch('/admin/api/bbs-settings')
.then(response => response.json())
.then(currentData => {
const credits = currentData.config?.credits || {};
const currentQwk = currentData.config?.qwk || {};
const config = {
features: {
webdoors: document.getElementById('bbsWebdoors').checked,
shoutbox: document.getElementById('bbsShoutbox').checked,
advertising: document.getElementById('bbsAdvertising').checked,
voting_booth: document.getElementById('bbsVoting').checked,
chat: document.getElementById('bbsChat').checked,
file_areas: document.getElementById('bbsFileAreas').checked,
guest_doors_page: document.getElementById('bbsGuestDoorsPage').checked,
bbs_directory: document.getElementById('bbsBbsDirectory').checked,
public_files_index: document.getElementById('bbsPublicFilesIndex') ? document.getElementById('bbsPublicFilesIndex').checked : false,
qwk: document.getElementById('bbsQwk').checked
},
credits: credits,
qwk: Object.assign({}, currentQwk, { bbs_id: qwkBbsIdRaw }),
max_cross_post_areas: maxCrossPost,
echomail_moderation_threshold: echomailModerationThreshold,
dashboard_ad_rotate_interval_seconds: dashboardAdRotateInterval,
default_echo_interface: document.getElementById('bbsDefaultEchoInterface').value,
bulletin_display_mode: document.getElementById('bbsBulletinDisplayMode').value,
outgoing_charset: document.getElementById('bbsOutgoingCharset').value,
};
return fetch('/admin/api/bbs-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config })
});
})
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(apiError(data, uiT('ui.admin.bbs_settings.save_failed', 'Failed to save settings')));
}
const successMessage = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.bbs_settings.saved_success', 'BBS features saved successfully.'))
: uiT('ui.admin.bbs_settings.saved_success', 'BBS features saved successfully.');
showBbsSettingsAlert(successMessage, 'success');
})
.catch(error => {
showBbsSettingsAlert(error.message || uiT('ui.admin.bbs_settings.save_failed', 'Failed to save settings'), 'danger');
});
}
function saveBbsPacketBbs() {
const sessionTimeout = parseInt(document.getElementById('bbsPacketBbsSessionTimeout').value, 10);
if (Number.isNaN(sessionTimeout) || sessionTimeout < 1) {
showBbsPacketBbsAlert(uiT('ui.admin.bbs_settings.validation.session_timeout_positive', 'Session timeout must be a positive integer.'), 'danger');
return;
}
fetch('/admin/api/bbs-settings')
.then(response => response.json())
.then(currentData => {
const config = Object.assign({}, currentData.config || {}, {
packet_bbs: {
session_timeout_minutes: sessionTimeout,
allow_guest_who: document.getElementById('bbsPacketBbsAllowGuestWho').checked
}
});
return fetch('/admin/api/bbs-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config })
});
})
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(apiError(data, uiT('ui.admin.bbs_settings.packet_bbs.save_failed', 'Failed to save Packet BBS settings.')));
}
const successMessage = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.bbs_settings.packet_bbs.saved_success', 'Packet BBS settings saved successfully.'))
: uiT('ui.admin.bbs_settings.packet_bbs.saved_success', 'Packet BBS settings saved successfully.');
showBbsPacketBbsAlert(successMessage, 'success');
})
.catch(error => {
showBbsPacketBbsAlert(error.message || uiT('ui.admin.bbs_settings.packet_bbs.save_failed', 'Failed to save Packet BBS settings.'), 'danger');
});
}
function saveTerminalIdle() {
const defaultShellSelect = document.getElementById('bbsTerminalDefaultShell');
if (!defaultShellSelect || defaultShellSelect.options.length === 0) {
showBbsTerminalIdleAlert(uiT('ui.admin.bbs_settings.terminal_server.save_failed', 'Failed to save terminal server settings.'), 'danger');
return;
}
const warnMinutes = parseInt(document.getElementById('bbsIdleWarnMinutes').value, 10);
if (Number.isNaN(warnMinutes) || warnMinutes < 1) {
showBbsTerminalIdleAlert(uiT('ui.admin.bbs_settings.validation.terminal_idle_warn_positive', 'Idle warning timeout must be a positive integer.'), 'danger');
return;
}
const disconnectMinutes = parseInt(document.getElementById('bbsIdleDisconnectMinutes').value, 10);
if (Number.isNaN(disconnectMinutes) || disconnectMinutes < 1) {
showBbsTerminalIdleAlert(uiT('ui.admin.bbs_settings.validation.terminal_idle_disconnect_positive', 'Idle disconnect timeout must be a positive integer.'), 'danger');
return;
}
if (disconnectMinutes <= warnMinutes) {
showBbsTerminalIdleAlert(uiT('ui.admin.bbs_settings.validation.terminal_idle_disconnect_gt_warn', 'Idle disconnect timeout must be greater than the warning timeout.'), 'danger');
return;
}
fetch('/admin/api/bbs-settings')
.then(response => response.json())
.then(currentData => {
const config = Object.assign({}, currentData.config || {}, {
terminal_idle: {
warn_minutes: warnMinutes,
disconnect_minutes: disconnectMinutes
},
terminal_server: {
default_shell: defaultShellSelect.value,
force_shell: document.getElementById('bbsTerminalForceShell').checked
}
});
return fetch('/admin/api/bbs-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config })
});
})
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(apiError(data, uiT('ui.admin.bbs_settings.terminal_server.save_failed', 'Failed to save terminal server settings.')));
}
const successMessage = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.bbs_settings.terminal_server.saved_success', 'Terminal server settings saved.'))
: uiT('ui.admin.bbs_settings.terminal_server.saved_success', 'Terminal server settings saved.');
showBbsTerminalIdleAlert(successMessage, 'success');
})
.catch(error => {
showBbsTerminalIdleAlert(error.message || uiT('ui.admin.bbs_settings.terminal_server.save_failed', 'Failed to save terminal server settings.'), 'danger');
});
}
function loadBbsSystem() {
fetch('/admin/api/bbs-system')
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(apiError(data, uiT('ui.admin.bbs_settings.load_system_failed', 'Failed to load system settings')));
}
const config = data.config || {};
document.getElementById('bbsSystemName').value = config.name || '';
document.getElementById('bbsSystemSysop').dataset.currentValue = config.sysop || '';
document.getElementById('bbsSystemLocation').value = config.location || '';
document.getElementById('bbsSystemTimezone').value = config.timezone || 'UTC';
document.getElementById('bbsSystemAddress').value = config.address || '';
document.getElementById('bbsSystemOrigin').value = config.origin || '';
syncSysopSelect('bbsSystemSysop');
})
.catch(error => {
showBbsSystemAlert(error.message || uiT('ui.admin.bbs_settings.load_system_failed', 'Failed to load system settings'), 'danger');
});
}
function saveBbsSystem() {
const config = {
name: document.getElementById('bbsSystemName').value.trim(),
sysop: document.getElementById('bbsSystemSysop').value,
location: document.getElementById('bbsSystemLocation').value.trim(),
timezone: document.getElementById('bbsSystemTimezone').value,
address: document.getElementById('bbsSystemAddress').value.trim(),
origin: document.getElementById('bbsSystemOrigin').value.trim()
};
fetch('/admin/api/bbs-system', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config })
})
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(apiError(data, uiT('ui.admin.bbs_settings.save_system_failed', 'Failed to save system settings')));
}
const successMessage = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.bbs_settings.system_saved_success', 'System settings saved successfully.'))
: uiT('ui.admin.bbs_settings.system_saved_success', 'System settings saved successfully.');
showBbsSystemAlert(successMessage, 'success');
})
.catch(error => {
showBbsSystemAlert(error.message || uiT('ui.admin.bbs_settings.save_system_failed', 'Failed to save system settings'), 'danger');
});
}
function saveBbsCredits() {
const symbol = document.getElementById('bbsCreditsSymbol').value.trim();
if (symbol.length > 5) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.currency_symbol_length', 'Currency symbol must be 0-5 characters.'), 'danger');
return;
}
const dailyAmount = parseInt(document.getElementById('bbsCreditsDailyAmount').value, 10);
if (Number.isNaN(dailyAmount) || dailyAmount < 0) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.daily_login_amount_non_negative', 'Daily login amount must be a non-negative integer.'), 'danger');
return;
}
const dailyDelay = parseInt(document.getElementById('bbsCreditsDailyDelay').value, 10);
if (Number.isNaN(dailyDelay) || dailyDelay < 0) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.daily_login_delay_non_negative', 'Daily login delay must be a non-negative integer.'), 'danger');
return;
}
const approvalBonus = parseInt(document.getElementById('bbsCreditsApprovalBonus').value, 10);
if (Number.isNaN(approvalBonus) || approvalBonus < 0) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.approval_bonus_non_negative', 'Approval bonus must be a non-negative integer.'), 'danger');
return;
}
const netmailCost = parseInt(document.getElementById('bbsCreditsNetmailCost').value, 10);
if (Number.isNaN(netmailCost) || netmailCost < 0) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.netmail_cost_non_negative', 'Netmail cost must be a non-negative integer.'), 'danger');
return;
}
const echomailReward = parseInt(document.getElementById('bbsCreditsEchomailReward').value, 10);
if (Number.isNaN(echomailReward) || echomailReward < 0) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.echomail_reward_non_negative', 'Echomail reward must be a non-negative integer.'), 'danger');
return;
}
const crashmailCost = parseInt(document.getElementById('bbsCreditsCrashmailCost').value, 10);
if (Number.isNaN(crashmailCost) || crashmailCost < 0) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.crashmail_cost_non_negative', 'Crashmail cost must be a non-negative integer.'), 'danger');
return;
}
const pollCreationCost = parseInt(document.getElementById('bbsCreditsPollCreationCost').value, 10);
if (Number.isNaN(pollCreationCost) || pollCreationCost < 0) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.poll_creation_cost_non_negative', 'Poll creation cost must be a non-negative integer.'), 'danger');
return;
}
const fileUploadCost = parseInt(document.getElementById('bbsCreditsFileUploadCost').value, 10);
if (Number.isNaN(fileUploadCost) || fileUploadCost < 0) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.file_upload_cost_non_negative', 'File upload cost must be a non-negative integer.'), 'danger');
return;
}
const fileUploadReward = parseInt(document.getElementById('bbsCreditsFileUploadReward').value, 10);
if (Number.isNaN(fileUploadReward) || fileUploadReward < 0) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.file_upload_reward_non_negative', 'File upload reward must be a non-negative integer.'), 'danger');
return;
}
const fileDownloadCost = parseInt(document.getElementById('bbsCreditsFileDownloadCost').value, 10);
if (Number.isNaN(fileDownloadCost) || fileDownloadCost < 0) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.file_download_cost_non_negative', 'File download cost must be a non-negative integer.'), 'danger');
return;
}
const fileDownloadReward = parseInt(document.getElementById('bbsCreditsFileDownloadReward').value, 10);
if (Number.isNaN(fileDownloadReward) || fileDownloadReward < 0) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.file_download_reward_non_negative', 'File download reward must be a non-negative integer.'), 'danger');
return;
}
const aiPerMilliUsd = parseInt(document.getElementById('bbsCreditsAiPerMilliUsd').value, 10);
if (Number.isNaN(aiPerMilliUsd) || aiPerMilliUsd < 0) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.ai_credits_per_milli_usd_non_negative', 'AI credits per $0.001 must be a non-negative integer.'), 'danger');
return;
}
const new14Days = parseInt(document.getElementById('bbsCreditsNew14Days').value, 10);
if (Number.isNaN(new14Days) || new14Days < 0) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.return_14_days_non_negative', '14-day return bonus must be a non-negative integer.'), 'danger');
return;
}
const transferFee = parseFloat(document.getElementById('bbsCreditsTransferFee').value);
if (Number.isNaN(transferFee) || transferFee < 0 || transferFee > 1) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.transfer_fee_range', 'Transfer fee must be between 0 and 1 (0% to 100%).'), 'danger');
return;
}
const referralBonus = parseInt(document.getElementById('bbsCreditsReferralBonus').value, 10);
if (Number.isNaN(referralBonus) || referralBonus < 0) {
showBbsCreditsAlert(uiT('ui.admin.bbs_settings.validation.referral_bonus_non_negative', 'Referral bonus must be a non-negative integer.'), 'danger');
return;
}
// Get current BBS features settings (don't change them)
fetch('/admin/api/bbs-settings')
.then(response => response.json())
.then(currentData => {
const features = currentData.config?.features || {};
const config = {
features: features,
credits: {
enabled: document.getElementById('bbsCreditsEnabled').checked,
symbol: symbol,
daily_amount: dailyAmount,
daily_login_delay_minutes: dailyDelay,
approval_bonus: approvalBonus,
netmail_cost: netmailCost,
echomail_reward: echomailReward,
crashmail_cost: crashmailCost,
poll_creation_cost: pollCreationCost,
file_upload_cost: fileUploadCost,
file_upload_reward: fileUploadReward,
file_download_cost: fileDownloadCost,
file_download_reward: fileDownloadReward,
ai_credits_per_milli_usd: aiPerMilliUsd,
return_14days: new14Days,
transfer_fee_percent: transferFee,
referral_enabled: document.getElementById('bbsCreditsReferralEnabled').checked,
referral_bonus: referralBonus
}
};
return fetch('/admin/api/bbs-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config })
});
})
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(apiError(data, uiT('ui.admin.bbs_settings.save_credits_failed', 'Failed to save credits settings')));
}
const successMessage = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.bbs_settings.credits_saved_success', 'Credits settings saved successfully.'))
: uiT('ui.admin.bbs_settings.credits_saved_success', 'Credits settings saved successfully.');
showBbsCreditsAlert(successMessage, 'success');
})
.catch(error => {
showBbsCreditsAlert(error.message || uiT('ui.admin.bbs_settings.save_credits_failed', 'Failed to save credits settings'), 'danger');
});
}
function loadBbsTaglines() {
fetch('/admin/api/taglines')
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(apiError(data, uiT('ui.admin.bbs_settings.load_taglines_failed', 'Failed to load taglines')));
}
document.getElementById('bbsTaglinesText').value = data.taglines || '';
})
.catch(error => {
showBbsTaglinesAlert(error.message || uiT('ui.admin.bbs_settings.load_taglines_failed', 'Failed to load taglines'), 'danger');
});
}
function saveBbsTaglines() {
const text = document.getElementById('bbsTaglinesText').value || '';
fetch('/admin/api/taglines', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ taglines: text })
})
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(apiError(data, uiT('ui.admin.bbs_settings.save_taglines_failed', 'Failed to save taglines')));
}
const successMessage = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.bbs_settings.taglines_saved_success', 'Taglines saved successfully.'))
: uiT('ui.admin.bbs_settings.taglines_saved_success', 'Taglines saved successfully.');
showBbsTaglinesAlert(successMessage, 'success');
})
.catch(error => {
showBbsTaglinesAlert(error.message || uiT('ui.admin.bbs_settings.save_taglines_failed', 'Failed to save taglines'), 'danger');
});
}
function showBbsSettingsAlert(message, type) {
const container = document.getElementById('bbsSettingsAlert');
container.innerHTML = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
}
function showBbsPacketBbsAlert(message, type) {
const container = document.getElementById('bbsPacketBbsAlert');
container.innerHTML = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
}
function showBbsTerminalIdleAlert(message, type) {
const container = document.getElementById('bbsTerminalIdleAlert');
container.innerHTML = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
}
function showBbsCreditsAlert(message, type) {
const container = document.getElementById('bbsCreditsAlert');
container.innerHTML = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
}
function showBbsTaglinesAlert(message, type) {
const container = document.getElementById('bbsTaglinesAlert');
container.innerHTML = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
}
function showBbsSystemAlert(message, type) {
const container = document.getElementById('bbsSystemAlert');
container.innerHTML = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
}
function loadAdminUsers(selectId) {
fetch('/admin/api/admin-users')
.then(response => response.json())
.then(data => {
const list = Array.isArray(data.admins) ? data.admins : [];
const select = document.getElementById(selectId);
select.innerHTML = '';
list.forEach(name => {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
select.appendChild(option);
});
syncSysopSelect(selectId);
})
.catch(() => {});
}
function syncSysopSelect(selectId) {
const select = document.getElementById(selectId);
const current = select.dataset.currentValue || '';
if (!current) {
return;
}
const exists = Array.from(select.options).some(opt => opt.value === current);
if (!exists) {
const option = document.createElement('option');
option.value = current;
option.textContent = `${current} (${uiT('ui.admin.bbs_settings.current', 'current')})`;
select.insertBefore(option, select.firstChild);
}
select.value = current;
}
function apiError(payload, fallback) {
if (window.getApiErrorMessage) {
return window.getApiErrorMessage(payload, fallback);
}
if (payload && payload.error) {
return String(payload.error);
}
return fallback;
}
</script>
{% endblock %}
|