<!DOCTYPE html>
<html lang="{{ locale|default('en') }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token }}">{# session-scoped CSRF token, stable across tabs #}
<title>{% block title %}{{ system_name }}{% endblock %}</title>
{# SEO description tags from appearance config; child templates may override these entirely #}
{% block meta_description_tags %}
{% if appearance.seo.description %}
<meta name="description" content="{{ appearance.seo.description | e }}">
<meta property="og:description" content="{{ appearance.seo.description | e }}">
{% endif %}
{% endblock %}
{% if appearance.seo.og_image_url %}
<meta property="og:image" content="{{ appearance.seo.og_image_url | e }}">
{% endif %}
{# Meta tags block for SEO - can be overridden by child templates #}
{% block meta_tags %}{% endblock %}
<!-- Favicons -->
<link rel="icon" type="image/svg+xml" href="{{ favicon_svg }}">
<link rel="icon" type="image/x-icon" href="{{ favicon_ico }}">
<link rel="apple-touch-icon" href="{{ favicon_png }}">
<link rel="manifest" href="/appmanifestjson">
<link href="/vendor/bootstrap-5.3.0/css/bootstrap.min.css" rel="stylesheet">
<link href="/vendor/fontawesome-6.4.0/css/all.min.css" rel="stylesheet">
<link id="theme-stylesheet" href="{{ stylesheet }}" rel="stylesheet">
<link href="/css/ansisys.css" rel="stylesheet">
<link href="/vendor/plyr-3.8.4/plyr.min.css" rel="stylesheet">
<link href="/css/binkplayer/binkplayer.css" rel="stylesheet">
{% block head %}{% endblock %}
<style>
.chat-menu-icon.unread,
.mrc-menu-icon.unread {
color: #ffc107 !important;
}
.messaging-menu-icon.unread {
color: #ffc107 !important;
}
.mail-menu-icon.unread {
color: #ffc107 !important;
}
.file-menu-icon.unread,
.file-menu-parent-link.unread,
.file-menu-link.unread {
color: #ffc107 !important;
}
.file-approvals-menu-link.unread {
color: #ffc107 !important;
}
.echomail-moderation-menu-link.unread,
.area-management-menu-link.unread,
.admin-menu-link.unread {
color: #ffc107 !important;
}
/* Nested dropdown submenu styles */
.dropdown-submenu {
position: relative;
}
.dropdown-submenu > .dropdown-menu {
top: 0;
left: 100%;
margin-top: -1px;
margin-left: -1px;
}
.dropdown-submenu:hover > .dropdown-menu {
display: block;
}
.dropdown-submenu > .dropdown-toggle::after {
transform: rotate(-90deg);
position: absolute;
right: 10px;
top: 50%;
margin-top: -3px;
}
/* Mobile responsive: stack submenu below on small screens */
@media (max-width: 768px) {
.dropdown-submenu > .dropdown-menu {
position: static !important;
margin-left: 1rem;
border-left: 2px solid rgba(0,0,0,0.1);
}
}
{% if appearance.branding.accent_color %}
:root { --fidonet-blue: {{ appearance.branding.accent_color | e }}; }
.bg-primary { background-color: {{ appearance.branding.accent_color | e }} !important; }
.btn-fidonet { background-color: {{ appearance.branding.accent_color | e }}; border-color: {{ appearance.branding.accent_color | e }}; }
{% endif %}
</style>
<script src="/vendor/bootstrap-5.3.0/js/bootstrap.bundle.min.js"></script>
<script src="/vendor/jquery-3.7.1/jquery-3.7.1.min.js"></script>
<script>
window.appLocale = {{ locale|json_encode|raw }};
window.appDefaultLocale = {{ default_locale|json_encode|raw }};
window.appI18nNamespaces = {{ i18n_namespaces|json_encode|raw }};
window.siteConfig = {
ansiRendererMode: {{ ansi_renderer_mode|json_encode|raw }},
mediaPlayerEnabled: {{ media_player_enabled ? 'true' : 'false' }},
sseTransportMode: {{ effective_sse_transport_mode|json_encode|raw }},
configuredRealtimeTransportMode: {{ configured_realtime_transport_mode|json_encode|raw }},
realtimeTransportMode: {{ effective_realtime_transport_mode|json_encode|raw }},
realtimeWsUrl: {{ realtime_ws_url|json_encode|raw }}
};
</script>
<script src="/js/user-storage.js"></script>
<script src="/js/app.js"></script>
<script src="/vendor/plyr-3.8.4/plyr.min.js"></script>
<script src="/js/binkplayer/binkplayer.js"></script>
<script src="/js/media-player.js"></script>
{% if current_user %}
<script src="/js/binkstream-client.js"></script>
{% endif %}
{% if current_user %}
<script src="/js/notifier.js"></script>
{% endif %}
{% if current_user.is_admin and admin_terminal_enabled %}
<script src="/js/xterm.js"></script>
<script src="/js/xterm-addon-fit.js"></script>
<link href="/css/xterm.css" rel="stylesheet">
<script src="/js/admin-terminal.js" defer></script>
{% endif %}
{# Include custom header insertions if available #}
{% include 'custom/header.insert.twig' ignore missing %}
{% if debug_ansi_not_perfect %}
<style>.ansi-art-container pre, .ansi-art { font-family: inherit !important; }</style>
{% elseif debug_ansi_use_consolas %}
<style>.ansi-art-container pre, .ansi-art { font-family: Consolas, monospace !important; }</style>
{% endif %}
</head>
<body>
<!-- Update notification toast -->
<div class="position-fixed top-0 end-0 p-3" style="z-index: 11000;">
<div id="updateToast" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="false">
<div class="toast-header bg-primary text-white">
<i class="fas fa-info-circle me-2"></i>
<strong class="me-auto">{{ t('ui.base.update_available', {}, 'common') }}</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body bg-light">
<small class="text-muted">{{ t('ui.base.new_version_ready', {}, 'common') }}</small>
<div class="mt-2">
<button class="btn btn-sm btn-primary w-100" onclick="activateUpdate()">
<i class="fas fa-sync-alt me-1"></i> {{ t('ui.base.reload_now', {}, 'common') }}
</button>
</div>
</div>
</div>
</div>
<nav class="navbar navbar-expand-xl navbar-dark bg-primary sticky-top">
<div class="container">
<a class="navbar-brand" href="/">
{% if appearance.branding.logo_url %}
<img src="{{ appearance.branding.logo_url | e }}" alt="{{ system_name | e }}" style="max-height:40px;" class="me-1">
{% else %}
<i class="fas fa-network-wired"></i>
{{ system_name }}
{% endif %}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
{% if current_user %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-envelope-open-text messaging-menu-icon" id="messagingMenuIcon"></i> {{ t('ui.base.messaging', {}, 'common') }}
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="/netmail">
<i class="fas fa-envelope mail-menu-icon" id="netmailMenuIcon"></i> {{ t('ui.base.netmail', {}, 'common') }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ default_echo_list == 'echolist' ? '/echolist' : '/echomail' }}">
<i class="fas fa-comments mail-menu-icon" id="echomailMenuIcon"></i> {{ t('ui.base.echomail', {}, 'common') }}
</a>
</li>
<li>
<a class="dropdown-item" href="/bulletins">
<i class="fas fa-clipboard-list"></i> {{ t('ui.base.bulletins', {}, 'common') }}
</a>
</li>
{% if bbs_feature_enabled('qwk') %}
<li>
<a class="dropdown-item" href="/qwk">
<i class="fas fa-envelope-open-text"></i> QWK Offline Mail
</a>
</li>
{% endif %}
{% if appearance.message_reader.email_link_url %}
<li>
<a class="dropdown-item" href="{{ appearance.message_reader.email_link_url | e }}">
<i class="fas fa-at"></i> {{ t('ui.base.email_link', {}, 'common') }}
</a>
</li>
{% endif %}
{% if bbs_feature_enabled('chat') %}
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="/chat">
<i class="fas fa-comments chat-menu-icon" id="chatMenuIcon"></i> {{ t('ui.base.local_chat', {}, 'common') }}
</a>
</li>
{% endif %}
{% if webdoors_active and bbs_feature_enabled('webdoors') and mrc_webdoor_enabled %}
<li>
<a class="dropdown-item" href="/games/mrc">
<i class="fas fa-satellite-dish mrc-menu-icon"></i> {{ t('ui.base.mrc_chat', {}, 'common') }}
</a>
</li>
{% endif %}
{% if appearance.message_reader.discord_url %}
<li>
<a class="dropdown-item" href="{{ appearance.message_reader.discord_url | e }}" target="_blank" rel="noopener noreferrer">
<i class="fab fa-discord"></i> {{ t('ui.admin.appearance.message_reader.discord_url', {}, 'common') }}
</a>
</li>
{% endif %}
</ul>
</li>
{% if webdoors_active and bbs_feature_enabled('webdoors') %}
<li class="nav-item">
<a class="nav-link" href="/games">
<i class="fas fa-gamepad"></i> {{ t('ui.base.doors_games', {}, 'common') }}
</a>
</li>
{% endif %}
{% if bbs_feature_enabled('file_areas') and current_user %}
{% if current_user.is_admin %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle file-menu-parent-link" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-file file-menu-icon" id="filesMenuIcon"></i> {{ t('ui.base.files', {}, 'common') }}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item file-menu-link" href="/files"><i class="fas fa-file me-1"></i> {{ t('ui.base.files', {}, 'common') }}</a></li>
<li><a class="dropdown-item file-approvals-menu-link" href="/admin/file-approvals"><i class="fas fa-check-circle me-1"></i> {{ t('ui.base.file_approvals', {}, 'common') }}</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link file-menu-link" href="/files">
<i class="fas fa-file file-menu-icon" id="filesMenuIcon"></i> {{ t('ui.base.files', {}, 'common') }}
</a>
</li>
{% endif %}
{% endif %}
{% if bbs_directory_enabled %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-list-alt"></i> {{ t('ui.base.bbs_lists', {}, 'common') }}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/bbs-directory"><i class="fas fa-server"></i> {{ t('ui.base.bbs_directory', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/nodelist"><i class="fas fa-sitemap"></i> {{ t('ui.base.nodelist', {}, 'common') }}</a></li>
{% if meshcore_nodes_available %}
<li><a class="dropdown-item" href="/packetbbs-nodes"><i class="fas fa-broadcast-tower"></i> {{ t('ui.base.meshcore_nodes', {}, 'common') }}</a></li>
{% endif %}
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="/nodelist">
<i class="fas fa-sitemap"></i> {{ t('ui.base.nodelist', {}, 'common') }}
</a>
</li>
{% endif %}
{# Custom navigation links (1-3 inline, 4+ as dropdown) #}
{% set customLinks = appearance.navigation.custom_links %}
{% if customLinks | length > 0 and customLinks | length <= 3 %}
{% for link in customLinks %}
<li class="nav-item">
<a class="nav-link" href="{{ link.url | e }}"{% if link.new_tab %} target="_blank" rel="noopener"{% endif %}>
{{ link.label | e }}
</a>
</li>
{% endfor %}
{% elseif customLinks | length > 3 %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-link"></i> {{ t('ui.about.links', {}, 'common') }}
</a>
<ul class="dropdown-menu">
{% for link in customLinks %}
<li>
<a class="dropdown-item" href="{{ link.url | e }}"{% if link.new_tab %} target="_blank" rel="noopener"{% endif %}>
{{ link.label | e }}
</a>
</li>
{% endfor %}
</ul>
</li>
{% endif %}
{% if current_user.is_admin %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle admin-menu-link" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-cog"></i> {{ t('ui.base.admin', {}, 'common') }}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/whosonline"><i class="fas fa-user-friends"></i> {{ t('ui.base.admin.whos_online', {}, 'common') }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/admin/"><i class="fas fa-tachometer-alt"></i> {{ t('ui.base.admin.dashboard', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/binkp"><i class="fas fa-network-wired"></i> {{ t('ui.base.admin.binkp_status', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/users"><i class="fas fa-users"></i> {{ t('ui.base.admin.manage_users', {}, 'common') }}</a></li>
<li><hr class="dropdown-divider"></li>
<li class="dropdown-submenu">
<a class="dropdown-item dropdown-toggle area-management-menu-link" href="#"><i class="fas fa-folder-tree"></i> {{ t('ui.base.admin.area_management', {}, 'common') }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/echoareas"><i class="fas fa-comments"></i> {{ t('ui.base.admin.echo_areas', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/subscriptions"><i class="fas fa-bookmark"></i> {{ t('ui.base.admin.subscriptions', {}, 'common') }}</a></li>
{% if interests_enabled %}
<li><a class="dropdown-item" href="/admin/interests"><i class="fas fa-layer-group"></i> {{ t('ui.base.admin.interests', {}, 'common') }}</a></li>
{% endif %}
<li><a class="dropdown-item" href="/admin/auto-feed"><i class="fas fa-rss"></i> {{ t('ui.base.admin.auto_feed', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/echomail-robots"><i class="fas fa-robot"></i> {{ t('ui.base.admin.echomail_robots', {}, 'common') }}</a></li>
<li><a class="dropdown-item echomail-moderation-menu-link" href="/admin/echomail-moderation"><i class="fas fa-shield-alt"></i> {{ t('ui.base.admin.echomail_moderation', {}, 'common') }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/fileareas"><i class="fas fa-folder-open"></i> {{ t('ui.base.admin.file_areas', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/file-approvals"><i class="fas fa-user-check"></i> {{ t('ui.base.admin.file_approvals', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/filearea-rules"><i class="fas fa-filter"></i> {{ t('ui.base.admin.file_area_rules', {}, 'common') }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/admin/lovlynet"><i class="fas fa-globe"></i> {{ t('ui.base.admin.lovlynet', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/areafix"><i class="fas fa-network-wired me-1"></i> {{ t('ui.base.admin.areafix', {}, 'common') }}</a></li>
</ul>
</li>
<li class="dropdown-submenu">
<a class="dropdown-item dropdown-toggle" href="#"><i class="fas fa-users"></i> {{ t('ui.base.admin.community', {}, 'common') }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/admin/bbs-directory"><i class="fas fa-server"></i> {{ t('ui.base.admin.bbs_directory', {}, 'common') }}</a></li>
<li class="dropdown-submenu">
<a class="dropdown-item dropdown-toggle" href="#"><i class="fas fa-comments"></i> {{ t('ui.base.admin.chat', {}, 'common') }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/admin/chat-rooms"><i class="fas fa-comment-dots"></i> {{ t('ui.base.admin.chat_rooms', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/mrc-settings"><i class="fas fa-satellite-dish"></i> {{ t('ui.base.admin.mrc_settings', {}, 'common') }}</a></li>
</ul>
</li>
<li><a class="dropdown-item" href="/admin/ai-bots"><i class="fas fa-robot"></i> {{ t('ui.base.admin.ai_bots', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/polls"><i class="fas fa-vote-yea"></i> {{ t('ui.base.admin.polls', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/shoutbox"><i class="fas fa-bullhorn"></i> {{ t('ui.base.admin.shoutbox', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/weather-config"><i class="fas fa-cloud-sun"></i> {{ t('ui.base.admin.weather_config', {}, 'common') }}</a></li>
</ul>
</li>
<li class="dropdown-submenu">
<a class="dropdown-item dropdown-toggle" href="#"><i class="fas fa-ad"></i> {{ t('ui.base.admin.ads_menu', {}, 'common') }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/admin/ads"><i class="fas fa-image"></i> {{ t('ui.base.admin.ads', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/ad-campaigns"><i class="fas fa-bullhorn"></i> {{ t('ui.base.admin.ad_campaigns', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/bulletins"><i class="fas fa-clipboard-list"></i> {{ t('ui.base.admin.bulletins', {}, 'common') }}</a></li>
</ul>
</li>
<li class="dropdown-submenu">
<a class="dropdown-item dropdown-toggle" href="#"><i class="fas fa-gamepad"></i> {{ t('ui.base.admin.doors', {}, 'common') }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/admin/dosdoors"><i class="fas fa-terminal"></i> {{ t('ui.base.admin.doors_dos', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/native-doors"><i class="fas fa-door-open"></i> {{ t('ui.base.admin.doors_native', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/webdoors"><i class="fas fa-globe"></i> {{ t('ui.base.admin.doors_web', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/jsdosdoors"><i class="fas fa-floppy-disk"></i> {{ t('ui.base.admin.doors_jsdos', {}, 'common') }}</a></li>
</ul>
</li>
<li><hr class="dropdown-divider"></li>
<li class="dropdown-submenu">
<a class="dropdown-item dropdown-toggle" href="#"><i class="fas fa-chart-line"></i> {{ t('ui.base.admin.analytics', {}, 'common') }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/admin/activity-stats"><i class="fas fa-chart-bar"></i> {{ t('ui.base.admin.activity_stats', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/ai-usage"><i class="fas fa-robot"></i> {{ t('ui.base.admin.ai_usage', {}, 'common') }}</a></li>
<li><a class="dropdown-item{% if not license_valid %} dropdown-item-locked{% endif %}" href="{{ license_valid ? '/admin/ad-analytics' : '#' }}"{% if not license_valid %} title="{{ t('ui.base.admin.registered_feature', {}, 'common') }}" onclick="return false;"{% endif %}><i class="fas fa-ad"></i> {{ t('ui.base.admin.ad_analytics', {}, 'common') }}{% if not license_valid %} <i class="fas fa-lock ms-1 small"></i>{% endif %}</a></li>
<li><a class="dropdown-item" href="/admin/sharing"><i class="fas fa-share-alt"></i> {{ t('ui.base.admin.sharing', {}, 'common') }}</a></li>
<li><a class="dropdown-item{% if not license_valid %} dropdown-item-locked{% endif %}" href="{{ license_valid ? '/admin/economy' : '#' }}"{% if not license_valid %} title="{{ t('ui.base.admin.registered_feature', {}, 'common') }}" onclick="return false;"{% endif %}><i class="fas fa-coins"></i> {{ t('ui.base.admin.economy_viewer', {}, 'common') }}{% if not license_valid %} <i class="fas fa-lock ms-1 small"></i>{% endif %}</a></li>
<li><a class="dropdown-item{% if not license_valid %} dropdown-item-locked{% endif %}" href="{{ license_valid ? '/admin/referrals' : '#' }}"{% if not license_valid %} title="{{ t('ui.base.admin.registered_feature', {}, 'common') }}" onclick="return false;"{% endif %}><i class="fas fa-user-plus"></i> {{ t('ui.base.admin.referrals', {}, 'common') }}{% if not license_valid %} <i class="fas fa-lock ms-1 small"></i>{% endif %}</a></li>
</ul>
</li>
<li class="dropdown-submenu">
<a class="dropdown-item dropdown-toggle" href="/admin/bbs-settings"><i class="fas fa-sliders-h"></i> {{ t('ui.base.admin.bbs_settings', {}, 'common') }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/admin/appearance"><i class="fas fa-palette"></i> {{ t('ui.base.admin.appearance', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/ai-settings"><i class="fas fa-robot"></i> {{ t('ui.base.admin.ai_settings', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/bbs-settings"><i class="fas fa-sliders-h"></i> {{ t('ui.base.admin.bbs_settings', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/binkp-config"><i class="fas fa-network-wired"></i> {{ t('ui.base.admin.binkp_configuration', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/networks"><i class="fas fa-globe"></i> {{ t('ui.base.admin.networks', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/packet-bbs"><i class="fas fa-broadcast-tower"></i> {{ t('ui.base.admin.packet_bbs', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/i18n-overrides"><i class="fas fa-language"></i> {{ t('ui.base.admin.i18n_overrides', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/licensing"><i class="fas fa-id-card"></i> {{ t('ui.base.admin.licensing', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/template-editor"><i class="fas fa-file-code"></i> {{ t('ui.base.admin.template_editor', {}, 'common') }}</a></li>
</ul>
</li>
<li><hr class="dropdown-divider"></li>
<li class="dropdown-submenu">
<a class="dropdown-item dropdown-toggle" href="#"><i class="fas fa-question-circle"></i> {{ t('ui.base.admin.help', {}, 'common') }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/admin/docs/view/README"><i class="fas fa-book"></i> {{ t('ui.base.admin.readme', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/docs"><i class="fas fa-book-open"></i> {{ t('ui.base.admin.docs', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/admin/docs/view/FAQ"><i class="fas fa-question-circle"></i> {{ t('ui.base.admin.faq', {}, 'common') }}</a></li>
{% if has_upgrading_doc %}<li><a class="dropdown-item" href="/admin/upgrade-notes"><i class="fas fa-arrow-up-from-bracket"></i> {{ t('ui.base.admin.upgrade_notes', {'version': app_version}, 'common') }}</a></li>{% endif %}
<li><a class="dropdown-item" href="https://claudes.lovelybits.org" target="_blank"><i class="fas fa-comments"></i> {{ t('ui.base.admin.claudes_bbs', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="https://github.com/awehttam/binkterm-php/issues" target="_blank"><i class="fas fa-bug"></i> {{ t('ui.base.admin.report_issue', {}, 'common') }}</a></li>
{% if not license_valid %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-primary" href="/admin/register"><i class="fas fa-id-card me-1"></i> {{ t('ui.base.admin.register', {}, 'common') }}</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li class="dropdown-submenu">
<a class="dropdown-item dropdown-toggle" href="#"><i class="fas fa-code"></i> Developer</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/admin/sse-test"><i class="fas fa-satellite-dish"></i> BinkStream Test</a></li>
<li><a class="dropdown-item" href="/admin/buffer-test"><i class="fas fa-filter"></i> Buffer Test</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
{% endif %}
{% else %}
{% if bbs_directory_enabled %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-list-alt"></i> {{ t('ui.base.bbs_lists', {}, 'common') }}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/bbs-directory"><i class="fas fa-server"></i> {{ t('ui.base.bbs_directory', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/nodelist"><i class="fas fa-sitemap"></i> {{ t('ui.base.nodelist', {}, 'common') }}</a></li>
{% if meshcore_nodes_available %}
<li><a class="dropdown-item" href="/packetbbs-nodes"><i class="fas fa-broadcast-tower"></i> {{ t('ui.base.meshcore_nodes', {}, 'common') }}</a></li>
{% endif %}
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="/nodelist">
<i class="fas fa-sitemap"></i> {{ t('ui.base.nodelist', {}, 'common') }}
</a>
</li>
{% endif %}
{% if bbs_feature_enabled('guest_doors_page') %}
<li class="nav-item">
<a class="nav-link" href="/guest-doors">
<i class="fas fa-terminal"></i> {{ t('ui.base.guest_doors', {}, 'common') }}
</a>
</li>
{% endif %}
{% if bbs_feature_enabled('public_files_index') %}
<li class="nav-item">
<a class="nav-link" href="/public-files">
<i class="fas fa-folder-open"></i> {{ t('ui.base.public_files', {}, 'common') }}
</a>
</li>
{% endif %}
{% if appearance.seo.about_page_enabled %}
<li class="nav-item">
<a class="nav-link" href="/about">
<i class="fas fa-info-circle"></i> {{ t('ui.about.title', {}, 'common') }}
</a>
</li>
{% endif %}
{% endif %}
</ul>
<ul class="navbar-nav">
{% if current_user %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
{% if credits_enabled %}
<span class="me-2" id="headerCreditBalance">{{ credits_symbol }}{{ credit_balance }}</span>
{% endif %}
<i class="fas fa-user"></i> {{ current_user.username }}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/profile"><i class="fas fa-id-card me-1"></i> {{ t('ui.base.profile', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/settings"><i class="fas fa-sliders-h me-1"></i> {{ t('ui.base.settings', {}, 'common') }}</a></li>
<li><a class="dropdown-item" href="/subscriptions"><i class="fas fa-bookmark"></i> {{ t('ui.base.subscriptions', {}, 'common') }}</a></li>
{% if interests_enabled and has_active_interests %}
<li><a class="dropdown-item" href="/interests"><i class="fas fa-layer-group"></i> {{ t('ui.interests.nav_label', {}, 'common') }}</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/user-guide"><i class="fas fa-book-open me-1"></i> {{ t('ui.base.user_guide', {}, 'common') }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="logout()">{{ t('ui.base.logout', {}, 'common') }}</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="/login">{{ t('ui.base.login', {}, 'common') }}</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
{# Announcement banner #}
{% if appearance.content.announcement._active %}
{% set ann = appearance.content.announcement %}
<div id="announcementBanner" class="alert alert-{{ ann.type | default('info') }} alert-dismissible mb-0 rounded-0 border-0 border-bottom"
role="alert" data-ann-key="{{ ann._key | e }}" style="display:none;">
<div class="container">
{{ ann.text | e }}
{% if ann.dismissible %}
<button type="button" class="btn-close" onclick="dismissAnnouncement('{{ ann._key | e }}')" aria-label="Close"></button>
{% endif %}
</div>
</div>
<script>
// Defer until DOMContentLoaded so window.currentUserId is set before
// UserStorage is called ? without it UserStorage falls back to sessionStorage
// and the dismissal is never found on the next page load.
document.addEventListener('DOMContentLoaded', function() {
var key = 'ann_dismissed_{{ ann._key | e }}';
if (!UserStorage.getItem(key)) {
document.getElementById('announcementBanner').style.display = '';
}
});
function dismissAnnouncement(annKey) {
UserStorage.setItem('ann_dismissed_' + annKey, '1');
document.getElementById('announcementBanner').style.display = 'none';
}
</script>
{% endif %}
<main class="container mt-4">
{% block content %}{% endblock %}
</main>
<footer class="bg-light mt-5 py-3">
<div class="container text-center">
<small class="text-muted">
{% if license_valid and appearance.branding.footer_text %}
{{ appearance.branding.footer_text | e }}
{% else %}
{{ system_name }} - Node {{ fidonet_origin }} - Sysop: {{ sysop_name }}
{% endif %}
</small>
<br>
<small class="text-muted">
{% if not (license_valid and appearance.branding.hide_powered_by) %}
{{ t('ui.footer.powered_by', {}, 'common') }} <a href="https://lovelybits.org/binktermphp">BinktermPHP</a> <a href="https://github.com/awehttam/binkterm-php">{{ app_version }}</a>{% if license_valid and license_system_name and appearance.branding.show_registration_badge != false %} - {{ t('ui.footer.registered_to', {'name': license_system_name}, 'common') }}{% endif %} |
{% endif %}
<a href="#" data-bs-toggle="modal" data-bs-target="#houseRulesModal">{{ t('ui.about.house_rules', {}, 'common') }}</a>
{% if appearance.seo.about_page_enabled %}
| <a href="/about">{{ t('ui.about.title', {}, 'common') }}</a>
{% endif %}
</small>
</div>
</footer>
<div class="modal fade" id="houseRulesModal" tabindex="-1" aria-labelledby="houseRulesModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="houseRulesModalLabel">{{ t('ui.about.house_rules', {}, 'common') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{% if appearance_houserules_html %}
{{ appearance_houserules_html | raw }}
{% else %}
{% include 'rules.twig' %}
{% endif %}
</div>
</div>
</div>
</div>
<script>
// Global user settings loaded from server
window.userSettings = {
timezone: 'America/Los_Angeles' // Default, will be loaded via AJAX
};
{% if current_user %}
window.currentUserId = {{ current_user.user_id ?? current_user.id }};
window.currentUserIsAdmin = {{ current_user.is_admin ? 'true' : 'false' }};
{% endif %}
window.isDevMode = {{ is_dev ? 'true' : 'false' }};
// Handle nested dropdown submenus
$(document).ready(function() {
// Show submenu on hover (desktop)
$('.dropdown-submenu').hover(
function() {
$(this).find('> .dropdown-menu').show();
},
function() {
$(this).find('> .dropdown-menu').hide();
}
);
// Handle click on submenu toggle
$('.dropdown-submenu > .dropdown-toggle').on('click', function(e) {
const href = $(this).attr('href');
// If it's a real link (not #), allow normal navigation on direct click
if (href && href !== '#') {
// On mobile or if submenu is not visible, toggle submenu instead
if ($(window).width() <= 768 || !$(this).next('.dropdown-menu').is(':visible')) {
e.preventDefault();
e.stopPropagation();
const $submenu = $(this).next('.dropdown-menu');
$('.dropdown-submenu .dropdown-menu').not($submenu).hide();
$submenu.toggle();
}
// Otherwise let the link work normally
} else {
// For # links, just toggle submenu
e.preventDefault();
e.stopPropagation();
const $submenu = $(this).next('.dropdown-menu');
$('.dropdown-submenu .dropdown-menu').not($submenu).hide();
$submenu.toggle();
}
});
// Hide submenu when parent dropdown closes
$('.dropdown').on('hide.bs.dropdown', function() {
$(this).find('.dropdown-submenu .dropdown-menu').hide();
});
});
</script>
<script>
if ('serviceWorker' in navigator) {
// Reload/update toast intentionally disabled.
// The service worker still updates and claims clients normally, but
// we no longer interrupt users with a reload prompt.
//
// let updateNotified = false;
//
// function showUpdateToast() {
// if (updateNotified) return;
// updateNotified = true;
// const toastEl = document.getElementById('updateToast');
// if (toastEl && window.bootstrap) {
// bootstrap.Toast.getOrCreateInstance(toastEl).show();
// }
// }
//
// Path 1: SW sends UPDATE_AVAILABLE after activate + clients.claim().
// navigator.serviceWorker.addEventListener('message', (event) => {
// if (event.data && event.data.type === 'UPDATE_AVAILABLE') {
// showUpdateToast();
// }
// });
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' })
.then(registration => {
// Path 1 (postMessage from SW) is the only notification mechanism.
// Path 2 (watchWorker statechange) was removed: it fired on every
// navigation where the browser detected a SW update and the new SW
// activated, causing a spurious reload prompt as the user navigated
// away. Path 1 is sufficient ? the SW already gates UPDATE_AVAILABLE
// on replacedOld (old caches were purged), so it only fires for real
// version upgrades on pages that were open before the update landed.
registration.update();
setInterval(() => registration.update(), 60000);
})
.catch(err => {
console.warn('[SW] Registration failed:', err);
});
});
}
function activateUpdate() {
// The SW already activated via skipWaiting() during install.
// A plain reload is all that is needed to pick up the new cached assets.
window.location.reload();
}
</script>
{% block scripts %}{% endblock %}
</body>
</html>
|