{% extends "base.twig" %}
{% block title %}{{ t('ui.echoareas.page_title', {}, locale, ['common']) }} - {{ parent() }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="fas fa-comments"></i>
{{ t('ui.echoareas.heading', {}, locale, ['common']) }}
</h2>
<div class="d-flex gap-2">
<a href="/echoareas/import" class="btn btn-outline-primary">
<i class="fas fa-file-import"></i>
{{ t('ui.common.import', {}, locale, ['common']) }}
</a>
<button class="btn btn-fidonet" onclick="showAddEchoareaModal()">
<i class="fas fa-plus"></i>
{{ t('ui.echoareas.add_echo_area', {}, locale, ['common']) }}
</button>
</div>
</div>
<div class="row">
<div class="col-lg-9">
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">{{ t('ui.echoareas.active_echo_areas', {}, locale, ['common']) }}</h6>
<div class="btn-group btn-group-sm" role="group">
<input type="radio" class="btn-check" name="filter" id="active" value="active" checked>
<label class="btn btn-outline-secondary" for="active">{{ t('ui.common.active', {}, locale, ['common']) }}</label>
<input type="radio" class="btn-check" name="filter" id="all" value="all">
<label class="btn btn-outline-secondary" for="all">{{ t('ui.common.all', {}, locale, ['common']) }}</label>
<input type="radio" class="btn-check" name="filter" id="inactive" value="inactive">
<label class="btn btn-outline-secondary" for="inactive">{{ t('ui.common.inactive', {}, locale, ['common']) }}</label>
</div>
</div>
<input type="text" class="form-control form-control-sm" id="echoareaSearch"
placeholder="{{ t('ui.echoareas.search_tags_placeholder', {}, locale, ['common']) }}" onkeyup="searchEchoareas(this.value)">
<div class="mt-2" id="echoareaDomainFilters"></div>
</div>
<div class="card-body p-0">
<div id="echoareasContainer">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin me-2"></i>
{{ t('ui.echoareas.loading_echo_areas', {}, locale, ['common']) }}
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3">
<div class="card">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.echoareas.statistics', {}, locale, ['common']) }}</h6>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-8">{{ t('ui.echoareas.active_areas', {}, locale, ['common']) }}</dt>
<dd class="col-4" id="activeCount">-</dd>
<dt class="col-8">{{ t('ui.echoareas.total_messages', {}, locale, ['common']) }}</dt>
<dd class="col-4" id="totalMessages">-</dd>
<dt class="col-8">{{ t('ui.echoareas.todays_messages', {}, locale, ['common']) }}</dt>
<dd class="col-4" id="todayMessages">-</dd>
</dl>
</div>
</div>
{#
<div class="card mt-3">
<div class="card-header">
<h6 class="mb-0">Quick Actions</h6>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<button class="btn btn-outline-primary" onclick="syncWithUplinks()">
<i class="fas fa-sync"></i>
Sync with Uplinks
</button>
<button class="btn btn-outline-secondary" onclick="exportConfiguration()">
<i class="fas fa-download"></i>
Export Configuration
</button>
<button class="btn btn-outline-warning" onclick="validateConfiguration()">
<i class="fas fa-check"></i>
Validate Configuration
</button>
</div>
</div>
</div>
#}
<div class="card mt-3">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.echoareas.color_legend', {}, locale, ['common']) }}</h6>
</div>
<div class="card-body">
<small class="text-muted">
{{ t('ui.echoareas.color_legend_help', {}, locale, ['common']) }}
</small>
<ul class="list-unstyled mt-2 small">
<li><span class="badge me-2" style="background-color: #28a745;">{{ t('ui.echoareas.color_green', {}, locale, ['common']) }}</span> {{ t('ui.echoareas.legend_general_topics', {}, locale, ['common']) }}</li>
<li><span class="badge me-2" style="background-color: #17a2b8;">{{ t('ui.echoareas.color_blue', {}, locale, ['common']) }}</span> {{ t('ui.echoareas.legend_technical_testing', {}, locale, ['common']) }}</li>
<li><span class="badge me-2" style="background-color: #dc3545;">{{ t('ui.echoareas.color_red', {}, locale, ['common']) }}</span> {{ t('ui.echoareas.legend_important_official', {}, locale, ['common']) }}</li>
<li><span class="badge me-2" style="background-color: #ffc107;">{{ t('ui.echoareas.color_yellow', {}, locale, ['common']) }}</span> {{ t('ui.echoareas.legend_administrative', {}, locale, ['common']) }}</li>
<li><span class="badge me-2" style="background-color: #6f42c1;">{{ t('ui.echoareas.color_purple', {}, locale, ['common']) }}</span> {{ t('ui.echoareas.legend_special_interest', {}, locale, ['common']) }}</li>
<li><span class="badge me-2" style="background-color: #fd7e14;">{{ t('ui.echoareas.color_orange', {}, locale, ['common']) }}</span> {{ t('ui.echoareas.legend_regional', {}, locale, ['common']) }}</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Add/Edit Echoarea Modal -->
<div class="modal fade" id="echoareaModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="echoareaModalTitle">{{ t('ui.echoareas.add_echo_area', {}, locale, ['common']) }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="echoareaModalError" class="alert alert-danger d-none" role="alert"></div>
<form id="echoareaForm">
<input type="hidden" id="echoareaId">
<div class="row g-4">
<div class="col-lg-7">
<div class="mb-3">
<label for="echoareaTag" class="form-label">{{ t('ui.echoareas.tag_required', {}, locale, ['common']) }}</label>
<input type="text" class="form-control text-uppercase" id="echoareaTag" name="tag" required maxlength="20">
<div class="form-text">{{ t('ui.echoareas.tag_help', {}, locale, ['common']) }}</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center">
<label for="echoareaDescription" class="form-label mb-0">{{ t('ui.echoareas.description_required', {}, locale, ['common']) }}</label>
<button type="button" class="btn btn-outline-secondary btn-sm d-none" id="echoareaLovlyNetSyncButton" onclick="syncLovlyNetEchoareaDescription()">
<i class="fas fa-sync-alt me-1"></i>
{{ t('ui.echoareas.lovlynet_sync_button', {}, locale, ['common']) }}
</button>
</div>
<textarea class="form-control" id="echoareaDescription" name="description" rows="2" required maxlength="255"></textarea>
<div class="form-text">{{ t('ui.echoareas.description_help', {}, locale, ['common']) }}</div>
</div>
<div class="row mb-3">
<div class="col-md-8">
<label for="echoareaUplink" class="form-label">{{ t('ui.echoareas.uplink_address', {}, locale, ['common']) }}</label>
<input type="text" class="form-control" id="echoareaUplink" name="uplink_address" maxlength="20">
<div class="form-text">{{ t('ui.echoareas.uplink_address_help', {}, locale, ['common']) }}</div>
</div>
<div class="col-md-4">
<label for="echoareaColor" class="form-label">{{ t('ui.echoareas.color', {}, locale, ['common']) }}</label>
<input type="hidden" id="echoareaColor" name="color" value="#28a745">
<div class="color-palette">
<div class="color-swatch" data-color="#dc3545" style="background-color: #dc3545;" title="{{ t('ui.echoareas.color_red', {}, locale, ['common']) }}"></div>
<div class="color-swatch" data-color="#fd7e14" style="background-color: #fd7e14;" title="{{ t('ui.echoareas.color_orange', {}, locale, ['common']) }}"></div>
<div class="color-swatch" data-color="#ffc107" style="background-color: #ffc107;" title="{{ t('ui.echoareas.color_yellow', {}, locale, ['common']) }}"></div>
<div class="color-swatch" data-color="#28a745" style="background-color: #28a745;" title="{{ t('ui.echoareas.color_green', {}, locale, ['common']) }}"></div>
<div class="color-swatch" data-color="#20c997" style="background-color: #20c997;" title="{{ t('ui.echoareas.color_teal', {}, locale, ['common']) }}"></div>
<div class="color-swatch" data-color="#17a2b8" style="background-color: #17a2b8;" title="{{ t('ui.echoareas.color_blue', {}, locale, ['common']) }}"></div>
<div class="color-swatch" data-color="#0dcaf0" style="background-color: #0dcaf0;" title="{{ t('ui.echoareas.color_light_blue', {}, locale, ['common']) }}"></div>
<div class="color-swatch" data-color="#0d6efd" style="background-color: #0d6efd;" title="{{ t('ui.echoareas.color_blue', {}, locale, ['common']) }}"></div>
<div class="color-swatch" data-color="#6610f2" style="background-color: #6610f2;" title="{{ t('ui.echoareas.color_indigo', {}, locale, ['common']) }}"></div>
<div class="color-swatch" data-color="#6f42c1" style="background-color: #6f42c1;" title="{{ t('ui.echoareas.color_purple', {}, locale, ['common']) }}"></div>
<div class="color-swatch" data-color="#d63384" style="background-color: #d63384;" title="{{ t('ui.echoareas.color_pink', {}, locale, ['common']) }}"></div>
<div class="color-swatch" data-color="#6c757d" style="background-color: #6c757d;" title="{{ t('ui.echoareas.color_gray', {}, locale, ['common']) }}"></div>
</div>
<div class="form-text mt-2">{{ t('ui.echoareas.selected_color', {}, locale, ['common']) }} <span id="selectedColorPreview" style="display: inline-block; width: 20px; height: 20px; border: 1px solid #ccc; vertical-align: middle; background-color: #28a745;"></span></div>
</div>
</div>
<div class="mb-3">
<label for="echoareaDomain" class="form-label">{{ t('ui.common.domain', {}, locale, ['common']) }}</label>
<select class="form-select" id="echoareaDomain" name="domain">
<option value="">{{ t('ui.echoareas.local', {}, locale, ['common']) }}</option>
{% for network in networks %}
<option value="{{ network.domain }}">{{ network.name }} ({{ network.domain }})</option>
{% endfor %}
</select>
<div class="form-text">{{ t('ui.echoareas.network_domain_help', {}, locale, ['common']) }}</div>
</div>
<div class="mb-0">
<label for="echoareaModerator" class="form-label">{{ t('ui.echoareas.moderator', {}, locale, ['common']) }}</label>
<input type="text" class="form-control" id="echoareaModerator" name="moderator" maxlength="100">
<div class="form-text">{{ t('ui.echoareas.moderator_help', {}, locale, ['common']) }}</div>
</div>
</div>
<div class="col-lg-5">
<div class="mb-3">
<label for="echoareaPostingNamePolicy" class="form-label">{{ t('ui.echoareas.posting_name_policy', {}, locale, ['common']) }}</label>
<select class="form-select" id="echoareaPostingNamePolicy" name="posting_name_policy">
<option value="inherit">{{ t('ui.echoareas.posting_name_policy_inherit', {}, locale, ['common']) }}</option>
<option value="real_name">{{ t('ui.echoareas.posting_name_policy_real_name', {}, locale, ['common']) }}</option>
<option value="username">{{ t('ui.echoareas.posting_name_policy_username', {}, locale, ['common']) }}</option>
</select>
<div class="form-text">{{ t('ui.echoareas.posting_name_policy_help', {}, locale, ['common']) }}</div>
</div>
<div class="mb-3">
<label for="echoareaArtFormatHint" class="form-label">{{ t('ui.echoareas.art_format_hint', {}, locale, ['common']) }}</label>
<select class="form-select" id="echoareaArtFormatHint" name="art_format_hint">
<option value="inherit">{{ t('ui.echoareas.art_format_hint_inherit', {}, locale, ['common']) }}</option>
<option value="ansi">{{ t('ui.echoareas.art_format_hint_ansi', {}, locale, ['common']) }}</option>
<option value="amiga_ansi">{{ t('ui.echoareas.art_format_hint_amiga_ansi', {}, locale, ['common']) }}</option>
</select>
<div class="form-text">{{ t('ui.echoareas.art_format_hint_help', {}, locale, ['common']) }}</div>
</div>
<div class="border rounded p-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="echoareaActive" name="is_active" checked>
<label class="form-check-label" for="echoareaActive">
{{ t('ui.common.active', {}, locale, ['common']) }}
</label>
</div>
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="echoareaLocal" name="is_local">
<label class="form-check-label" for="echoareaLocal">
{{ t('ui.echoareas.local_only', {}, locale, ['common']) }}
</label>
<div class="form-text">{{ t('ui.echoareas.local_only_help', {}, locale, ['common']) }}</div>
</div>
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="echoareaSysopOnly" name="is_sysop_only">
<label class="form-check-label" for="echoareaSysopOnly">
{{ t('ui.echoareas.sysop_access_only', {}, locale, ['common']) }}
</label>
<div class="form-text">{{ t('ui.echoareas.sysop_access_only_help', {}, locale, ['common']) }}</div>
</div>
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="echoareaGeminiPublic" name="gemini_public">
<label class="form-check-label" for="echoareaGeminiPublic">
{{ t('ui.echoareas.public_gemini_access', {}, locale, ['common']) }}
</label>
<div class="form-text">{{ t('ui.echoareas.public_gemini_access_help', {}, locale, ['common']) }}</div>
</div>
<div class="mt-3">
<label for="echoareaAllowMedia" class="form-label">{{ t('ui.echoareas.allow_media', {}, locale, ['common']) }}</label>
<select class="form-select" id="echoareaAllowMedia" name="allow_media">
<option value="inherit">{{ t('ui.echoareas.allow_media_inherit', {}, locale, ['common']) }}</option>
<option value="allow">{{ t('ui.echoareas.allow_media_allow', {}, locale, ['common']) }}</option>
<option value="deny">{{ t('ui.echoareas.allow_media_deny', {}, locale, ['common']) }}</option>
</select>
<div class="form-text">{{ t('ui.echoareas.allow_media_help', {}, locale, ['common']) }}</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel', {}, locale, ['common']) }}</button>
<button type="button" class="btn btn-fidonet" onclick="saveEchoarea()">
<i class="fas fa-save"></i>
{{ t('ui.common.save', {}, locale, ['common']) }}
</button>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ t('ui.common.confirm_delete', {}, locale, ['common']) }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>{{ t('ui.echoareas.delete_confirm_prefix', {}, locale, ['common']) }} <strong id="deleteEchoareaName"></strong>?</p>
<p class="text-danger"><i class="fas fa-exclamation-triangle"></i> {{ t('ui.echoareas.delete_confirm_warning', {}, locale, ['common']) }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel', {}, locale, ['common']) }}</button>
<button type="button" class="btn btn-danger" id="confirmDelete">
<i class="fas fa-trash"></i>
{{ t('ui.common.delete', {}, locale, ['common']) }}
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<style>
.color-palette {
display: flex !important;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
margin-bottom: 8px;
width: 100%;
}
.color-swatch {
display: block !important;
width: calc((100% - 40px) / 6);
height: 35px;
min-height: 35px;
border-radius: 4px;
cursor: pointer;
border: 3px solid transparent;
transition: all 0.2s ease;
box-sizing: border-box;
flex-shrink: 0;
}
.color-swatch:hover {
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.color-swatch.selected {
border-color: #000;
box-shadow: 0 0 0 1px #fff, 0 0 0 3px #000;
}
.sortable-col {
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.sortable-col:hover {
background-color: rgba(0, 0, 0, 0.05);
}
</style>
<script>
let currentFilter = 'active';
let currentEchoareaId = null;
let allEchoareas = [];
let searchQuery = '';
let sortColumn = 'id';
let sortDirection = 'asc';
let selectedDomains = [];
let availableDomains = [];
let pendingEchoareaTag = null;
let pendingEchoareaDomain = null;
let pendingEchoareaOpenHandled = false;
let currentEchoareaInfo = null;
const configuredNetworkDomains = {{ networks|map(n => n.domain)|json_encode|raw }};
function apiError(payload, fallback) {
if (window.getApiErrorMessage) {
return window.getApiErrorMessage(payload, fallback);
}
return payload && payload.error ? payload.error : fallback;
}
function uiT(key, fallback, params = {}) {
if (window.t) {
return window.t(key, params, fallback);
}
return fallback;
}
$(document).ready(function() {
const params = new URLSearchParams(window.location.search);
pendingEchoareaTag = (params.get('tag') || '').trim().toUpperCase();
pendingEchoareaDomain = (params.get('domain') || '').trim().toLowerCase();
loadI18nNamespaces(['common', 'errors']).then(function() {
loadEchoareas();
loadStats();
});
// Filter change handler
$('input[name="filter"]').change(function() {
currentFilter = $(this).val();
loadEchoareas();
});
// Form validation
$('#echoareaTag').on('input', function() {
$(this).val($(this).val().toUpperCase());
});
// Color palette click handler - use event delegation
$(document).on('click', '.color-swatch', function() {
const color = $(this).data('color');
$('#echoareaColor').val(color);
$('#selectedColorPreview').css('background-color', color);
// Update selected state
$('.color-swatch').removeClass('selected');
$(this).addClass('selected');
});
});
function loadEchoareas() {
showLoading('#echoareasContainer');
let url = '/api/echoareas';
if (currentFilter !== 'all') {
url += `?filter=${currentFilter}`;
}
$.get(url)
.done(function(data) {
allEchoareas = data.echoareas;
updateAvailableDomains();
renderDomainFilters();
applySearchFilter();
openPendingEchoareaFromUrl();
})
.fail(function() {
$('#echoareasContainer').html(`<div class="text-center text-danger py-4">${uiT('ui.echoareas.load_failed', 'Failed to load echo areas')}</div>`);
});
}
function openPendingEchoareaFromUrl() {
if (pendingEchoareaOpenHandled || !pendingEchoareaTag) {
return;
}
const match = allEchoareas.find(function(area) {
const areaTag = String(area.tag || '').trim().toUpperCase();
const areaDomain = String(area.domain || '').trim().toLowerCase();
return areaTag === pendingEchoareaTag && areaDomain === pendingEchoareaDomain;
});
if (!match) {
return;
}
pendingEchoareaOpenHandled = true;
editEchoarea(match.id);
}
function searchEchoareas(query) {
searchQuery = query.toLowerCase();
applySearchFilter();
}
function updateAvailableDomains() {
const domains = new Set();
allEchoareas.forEach(function(area) {
const domain = String(area.domain || '').trim().toLowerCase();
domains.add(domain);
});
availableDomains = Array.from(domains).sort();
selectedDomains = selectedDomains.filter(function(domain) {
return availableDomains.includes(domain);
});
}
function renderDomainFilters() {
const $container = $('#echoareaDomainFilters');
if (!availableDomains.length) {
$container.empty();
return;
}
let html = `<div class="small text-muted mb-1">${uiT('ui.common.domain', 'Domain')}</div>`;
html += '<div class="d-flex flex-wrap gap-2">';
html += `<button type="button" class="btn btn-sm ${selectedDomains.length === 0 ? 'btn-primary' : 'btn-outline-secondary'}" onclick="clearDomainFilters()">${uiT('ui.common.all', 'All')}</button>`;
availableDomains.forEach(function(domain) {
const selected = selectedDomains.includes(domain);
const label = domain === '' ? uiT('ui.echoareas.local', 'Local') : domain;
html += `<button type="button" class="btn btn-sm ${selected ? 'btn-primary' : 'btn-outline-secondary'}" onclick="toggleDomainFilter('${escapeJsString(domain)}')">${escapeHtml(label)}</button>`;
});
html += '</div>';
$container.html(html);
}
function toggleDomainFilter(domain) {
if (selectedDomains.includes(domain)) {
selectedDomains = selectedDomains.filter(function(value) { return value !== domain; });
} else {
selectedDomains.push(domain);
selectedDomains.sort();
}
renderDomainFilters();
applySearchFilter();
}
function clearDomainFilters() {
selectedDomains = [];
renderDomainFilters();
applySearchFilter();
}
function setSortColumn(col) {
if (sortColumn === col) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else {
sortColumn = col;
sortDirection = 'asc';
}
applySearchFilter();
}
function sortIndicator(col) {
if (sortColumn !== col) {
return '<i class="fas fa-sort ms-1" style="font-size:0.75em;opacity:0.35;"></i>';
}
return sortDirection === 'asc'
? '<i class="fas fa-sort-up ms-1" style="font-size:0.75em;"></i>'
: '<i class="fas fa-sort-down ms-1" style="font-size:0.75em;"></i>';
}
function sortableHeader(label, col, width) {
const active = sortColumn === col ? ' text-primary' : '';
return `<th width="${width}" class="sortable-col${active}" onclick="setSortColumn('${col}')">${label}${sortIndicator(col)}</th>`;
}
function buildLovlyNetIssueMessages(area) {
const issues = Array.isArray(area.lovlynet_setting_issues) ? area.lovlynet_setting_issues : [];
const messages = [];
issues.forEach(function(issue) {
if (!issue || issue.setting !== 'sysop_only') {
return;
}
if (issue.recommended) {
messages.push(uiT('ui.echoareas.lovlynet_issue_sysop_only_on', 'LovlyNet recommends enabling Sysop Access Only'));
} else {
messages.push(uiT('ui.echoareas.lovlynet_issue_sysop_only_off', 'LovlyNet recommends disabling Sysop Access Only'));
}
});
return messages;
}
function buildLovlyNetIssueIcon(area) {
const messages = buildLovlyNetIssueMessages(area);
if (!messages.length) {
return '';
}
const tooltip = `${uiT('ui.echoareas.lovlynet_issue_title', 'LovlyNet recommended settings mismatch')}: ${messages.join('; ')}`;
return `<i class="fas fa-exclamation-triangle text-warning ms-2" title="${escapeHtmlAttr(tooltip)}"></i>`;
}
function applySearchFilter() {
let filtered = allEchoareas;
if (selectedDomains.length > 0) {
filtered = filtered.filter(function(area) {
const domain = String(area.domain || '').trim().toLowerCase();
return selectedDomains.includes(domain);
});
}
if (searchQuery.length > 0) {
filtered = filtered.filter(area =>
area.tag.toLowerCase().includes(searchQuery) ||
(area.description && area.description.toLowerCase().includes(searchQuery))
);
}
if (sortColumn) {
const numericCols = ['id', 'message_count', 'subscriber_count'];
filtered = [...filtered].sort((a, b) => {
let aVal = a[sortColumn];
let bVal = b[sortColumn];
if (aVal == null) aVal = numericCols.includes(sortColumn) ? 0 : '';
if (bVal == null) bVal = numericCols.includes(sortColumn) ? 0 : '';
if (numericCols.includes(sortColumn)) {
return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
}
aVal = String(aVal).toLowerCase();
bVal = String(bVal).toLowerCase();
if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
return 0;
});
}
displayEchoareas(filtered);
}
function displayEchoareas(echoareas) {
const container = $('#echoareasContainer');
let html = '';
if (echoareas.length === 0) {
html = `<div class="text-center text-muted py-4">${uiT('ui.echoareas.none_found', 'No echo areas found')}</div>`;
} else {
html = `
<div class="table-responsive">
<table class="table table-hover mb-0" style="table-layout: fixed;">
<thead>
<tr>
<th width="4%"></th>
${sortableHeader(uiT('ui.common.id', 'ID'), 'id', '5%')}
${sortableHeader(uiT('ui.echoareas.tag', 'Tag'), 'tag', '22%')}
${sortableHeader(uiT('ui.common.description', 'Description'), 'description', '33%')}
${sortableHeader('Msgs', 'message_count', '10%')}
${sortableHeader('Subs', 'subscriber_count', '9%')}
${sortableHeader(uiT('ui.echoareas.uplink', 'Uplink'), 'uplink_address', '13%')}
<th width="10%">${uiT('ui.common.actions', 'Actions')}</th>
</tr>
</thead>
<tbody>
`;
echoareas.forEach(function(area) {
const isActive = area.is_active;
const rowClass = isActive ? '' : 'table-secondary';
const statusIcon = isActive ?
`<i class="fas fa-check-circle text-success" title="${uiT('ui.common.active', 'Active')}"></i>` :
`<i class="fas fa-pause-circle text-warning" title="${uiT('ui.common.inactive', 'Inactive')}"></i>`;
const lovlyNetIssueIcon = buildLovlyNetIssueIcon(area);
html += `
<tr class="${rowClass}">
<td class="text-center">${statusIcon}</td>
<td>${area.id}</td>
<td>
<div>
<div class="d-flex align-items-center mb-1">
<span class="me-2" style="display: inline-block; width: 20px; height: 20px; min-width: 20px; min-height: 20px; background-color: ${area.color || '#28a745'}; border-radius: 3px; flex-shrink: 0;"></span>
<span style="color: #ffc107; word-break: break-all;">${escapeHtml(area.tag)}</span>${lovlyNetIssueIcon}
</div>
<div class="ms-4">
${area.domain ? `<span class="badge bg-secondary me-1" style="font-size: 0.7em;">${escapeHtml(area.domain)}</span>` : ''}
${area.is_local ? `<span class="badge bg-info me-1" style="font-size: 0.7em;" title="${uiT('ui.echoareas.local_only_title', 'Local only - not transmitted to uplinks')}">${uiT('ui.echoareas.local', 'Local')}</span>` : ''}
${area.is_sysop_only ? `<span class="badge bg-warning text-dark me-1" style="font-size: 0.7em;" title="${uiT('ui.echoareas.sysop_access_only_title', 'Sysop access only')}">${uiT('ui.echoareas.sysop', 'Sysop')}</span>` : ''}
</div>
</div>
</td>
<td style="word-break: break-word; overflow-wrap: break-word;">
<strong>${escapeHtml(area.description)}</strong>
${area.moderator ? `<br><small class="text-muted">${uiT('ui.echoareas.mod_prefix', 'Mod:')} ${escapeHtml(area.moderator)}</small>` : ''}
</td>
<td>
<span class="badge bg-secondary">${area.message_count || 0}</span>
</td>
<td>
<span class="badge bg-info text-dark">${area.subscriber_count || 0}</span>
</td>
<td>
<small class="font-monospace">${escapeHtml(area.uplink_address || uiT('ui.echoareas.uplink', 'Uplink'))}@${escapeHtml(area.domain||'')}</small>
</td>
<td>
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-primary" onclick="editEchoarea(${area.id})" title="${uiT('ui.common.edit', 'Edit')}">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="showDeleteModal(${area.id}, '${escapeHtml(area.tag)}')" title="${uiT('ui.common.delete', 'Delete')}">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
}
container.html(html);
}
function showAddEchoareaModal() {
currentEchoareaId = null;
currentEchoareaInfo = null;
$('#echoareaModalTitle').text(uiT('ui.echoareas.add_echo_area', 'Add Echo Area'));
$('#echoareaModalError').addClass('d-none').text('');
$('#echoareaForm')[0].reset();
$('#echoareaId').val('');
$("#echoareaDomain").val('');
$('#echoareaColor').val('#28a745');
$('#echoareaActive').prop('checked', true);
$('#echoareaLocal').prop('checked', false);
$('#echoareaSysopOnly').prop('checked', false);
$('#echoareaGeminiPublic').prop('checked', false);
$('#echoareaPostingNamePolicy').val('inherit');
$('#echoareaArtFormatHint').val('inherit');
$('#echoareaAllowMedia').val('inherit');
$('#echoareaLovlyNetSyncButton').addClass('d-none').prop('disabled', false).html(
`<i class="fas fa-sync-alt me-1"></i>${uiT('ui.echoareas.lovlynet_sync_button', 'Sync')}`
);
updateColorSelection('#28a745');
$('#echoareaModal').modal('show');
}
function ensureEchoareaDomainOption(domain) {
if (!domain || configuredNetworkDomains.includes(domain)) {
return;
}
const select = document.getElementById('echoareaDomain');
if (!select || Array.from(select.options).some(option => option.value === domain)) {
return;
}
const option = document.createElement('option');
option.value = domain;
option.textContent = `${domain} (${uiT('ui.admin.binkp_config.uplinks.modal.unconfigured_network', 'unconfigured')})`;
select.appendChild(option);
}
function editEchoarea(id) {
currentEchoareaId = id;
$('#echoareaModalTitle').text(uiT('ui.echoareas.edit_echo_area', 'Edit Echo Area'));
$('#echoareaModalError').addClass('d-none').text('');
$.get(`/api/echoareas/${id}`)
.done(function(data) {
const area = data.echoarea;
currentEchoareaInfo = area;
$('#echoareaTag').val(area.tag);
$('#echoareaDescription').val(area.description);
ensureEchoareaDomainOption(area.domain || '');
$("#echoareaDomain").val(area.domain || '');
$('#echoareaUplink').val(area.uplink_address || '');
$('#echoareaColor').val(area.color || '#28a745');
$('#echoareaModerator').val(area.moderator || '');
$('#echoareaActive').prop('checked', area.is_active);
$('#echoareaLocal').prop('checked', area.is_local);
$('#echoareaSysopOnly').prop('checked', area.is_sysop_only);
$('#echoareaGeminiPublic').prop('checked', area.gemini_public);
$('#echoareaPostingNamePolicy').val(area.posting_name_policy || 'inherit');
$('#echoareaArtFormatHint').val(area.art_format_hint || 'inherit');
const av = area.allow_media;
$('#echoareaAllowMedia').val(av === null || av === undefined ? 'inherit' : (av ? 'allow' : 'deny'));
toggleLovlyNetSyncButton(area);
updateColorSelection(area.color || '#28a745');
$('#echoareaModal').modal('show');
})
.fail(function() {
showError(uiT('ui.echoareas.load_details_failed', 'Failed to load echo area details'));
});
}
function toggleLovlyNetSyncButton(area) {
const isLovlyNet = String(area.domain || '').trim().toLowerCase() === 'lovlynet';
const needsSync = !!area.description_mismatch || !!area.lovlynet_has_setting_issues;
$('#echoareaLovlyNetSyncButton').toggleClass('d-none', !(isLovlyNet && needsSync));
}
function syncLovlyNetEchoareaDescription() {
if (!currentEchoareaId) {
return;
}
const $button = $('#echoareaLovlyNetSyncButton');
const areaTag = String($('#echoareaTag').val() || '').trim().toUpperCase();
$button.prop('disabled', true).html(`<i class="fas fa-spinner fa-spin me-1"></i>${uiT('ui.echoareas.lovlynet_sync_button_loading', 'Syncing...')}`);
$.ajax({
url: '/admin/api/lovlynet/area-sync',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ area_type: 'echo', area_tag: areaTag }),
success: function(response) {
$('#echoareaDescription').val(response.description || '');
$('#echoareaSysopOnly').prop('checked', !!(currentEchoareaInfo && currentEchoareaInfo.lovlynet_metadata && currentEchoareaInfo.lovlynet_metadata.sysop_only));
if (currentEchoareaInfo) {
currentEchoareaInfo.description_mismatch = false;
currentEchoareaInfo.lovlynet_has_setting_issues = false;
currentEchoareaInfo.lovlynet_setting_issues = [];
}
toggleLovlyNetSyncButton(currentEchoareaInfo || {});
const fallbackMessage = uiT('ui.echoareas.lovlynet_sync_success', 'Description updated from LovlyNet');
const successMessage = window.getApiMessage
? window.getApiMessage(response, fallbackMessage)
: fallbackMessage;
showSuccess(successMessage);
loadEchoareas();
},
error: function(xhr) {
const error = apiError(xhr.responseJSON, uiT('ui.echoareas.lovlynet_sync_failed', 'Failed to sync description from LovlyNet'));
$('#echoareaModalError').text(error).removeClass('d-none');
},
complete: function() {
$button.prop('disabled', false).html(
`<i class="fas fa-sync-alt me-1"></i>${uiT('ui.echoareas.lovlynet_sync_button', 'Sync')}`
);
}
});
}
function saveEchoarea() {
const form = $('#echoareaForm');
if (!form[0].checkValidity()) {
form[0].reportValidity();
return;
}
const selectedDomain = String($("#echoareaDomain").val() || '').trim();
if (selectedDomain !== '' && !configuredNetworkDomains.includes(selectedDomain)) {
$('#echoareaModalError')
.text(uiT('errors.echoareas.unknown_domain', 'Select a configured network domain'))
.removeClass('d-none');
return;
}
const formData = {
tag: $('#echoareaTag').val().toUpperCase(),
description: $('#echoareaDescription').val(),
uplink_address: $('#echoareaUplink').val() || null,
color: $('#echoareaColor').val(),
moderator: $('#echoareaModerator').val() || null,
is_active: $('#echoareaActive').is(':checked'),
is_local: $('#echoareaLocal').is(':checked'),
is_sysop_only: $('#echoareaSysopOnly').is(':checked'),
gemini_public: $('#echoareaGeminiPublic').is(':checked'),
domain: selectedDomain || null,
posting_name_policy: $('#echoareaPostingNamePolicy').val(),
art_format_hint: $('#echoareaArtFormatHint').val(),
allow_media: $('#echoareaAllowMedia').val()
};
const url = currentEchoareaId ? `/api/echoareas/${currentEchoareaId}` : '/api/echoareas';
const method = currentEchoareaId ? 'PUT' : 'POST';
$.ajax({
url: url,
method: method,
contentType: 'application/json',
data: JSON.stringify(formData),
success: function(response) {
$('#echoareaModal').modal('hide');
const fallbackMessage = currentEchoareaId
? uiT('ui.echoareas.updated_success', 'Echo area updated successfully')
: uiT('ui.echoareas.created_success', 'Echo area created successfully');
const successMessage = window.getApiMessage
? window.getApiMessage(response, fallbackMessage)
: fallbackMessage;
showSuccess(successMessage);
loadEchoareas();
loadStats();
},
error: function(xhr) {
const error = apiError(
xhr.responseJSON,
currentEchoareaId
? uiT('errors.echoareas.update_failed', 'Failed to update echo area')
: uiT('errors.echoareas.create_failed', 'Failed to create echo area')
);
$('#echoareaModalError').text(error).removeClass('d-none');
}
});
}
function showDeleteModal(id, tag) {
currentEchoareaId = id;
$('#deleteEchoareaName').text(tag);
$('#deleteModal').modal('show');
$('#confirmDelete').off('click').on('click', function() {
deleteEchoarea();
});
}
function deleteEchoarea() {
$.ajax({
url: `/api/echoareas/${currentEchoareaId}`,
method: 'DELETE',
success: function(response) {
$('#deleteModal').modal('hide');
const successMessage = window.getApiMessage
? window.getApiMessage(response, uiT('ui.echoareas.deleted_success', 'Echo area deleted successfully'))
: uiT('ui.echoareas.deleted_success', 'Echo area deleted successfully');
showSuccess(successMessage);
loadEchoareas();
loadStats();
},
error: function(xhr) {
$('#deleteModal').modal('hide');
const error = apiError(xhr.responseJSON, uiT('errors.echoareas.delete_failed', 'Failed to delete echo area'));
showError(error);
}
});
}
function loadStats() {
$.get('/api/echoareas/stats')
.done(function(data) {
$('#activeCount').text(data.active_count || 0);
$('#totalMessages').text(data.total_messages || 0);
$('#todayMessages').text(data.today_messages || 0);
});
}
function syncWithUplinks() {
showSuccess(uiT('ui.echoareas.sync_not_implemented', 'Sync functionality not yet implemented'));
}
function exportConfiguration() {
showSuccess(uiT('ui.echoareas.export_not_implemented', 'Export functionality not yet implemented'));
}
function validateConfiguration() {
showSuccess(uiT('ui.echoareas.validate_not_implemented', 'Validation functionality not yet implemented'));
}
function updateColorSelection(color) {
// Update the hidden input
$('#echoareaColor').val(color);
// Update the preview
$('#selectedColorPreview').css('background-color', color);
// Update selected state on swatches
$('.color-swatch').removeClass('selected');
$(`.color-swatch[data-color="${color}"]`).addClass('selected');
}
function escapeHtmlAttr(value) {
return escapeHtml(value)
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function escapeJsString(value) {
return String(value)
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'");
}
</script>
{% endblock %}
|