PHP Classes

File: templates/echoareas.twig

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   templates/echoareas.twig   Download  
File: templates/echoareas.twig
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 45,418 bytes
 

Contents

Class file image Download
{% 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, '&quot;') .replace(/'/g, '&#39;'); } function escapeJsString(value) { return String(value) .replace(/\\/g, '\\\\') .replace(/'/g, "\\'"); } </script> {% endblock %}