mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-03-29 03:19:51 +00:00
- Replaced direct SQLite connection calls with a context manager in various modules to ensure proper resource management and prevent file descriptor leaks. - Introduced a new `connection` method in `DBManager` to standardize connection handling. - Updated all relevant database interactions in modules such as `feed_manager`, `scheduler`, `commands`, and others to utilize the new connection method. - Improved code readability and maintainability by consolidating connection logic.
2767 lines
91 KiB
Python
Executable File
2767 lines
91 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Generate Command Reference Website
|
|
Creates a single-page HTML website with bot introduction and command reference
|
|
"""
|
|
|
|
import configparser
|
|
import sqlite3
|
|
import os
|
|
import html
|
|
import logging
|
|
import sys
|
|
import argparse
|
|
from pathlib import Path
|
|
from typing import Dict, List, Tuple, Optional, Any
|
|
from collections import defaultdict
|
|
from contextlib import closing
|
|
|
|
# Import bot modules with error handling
|
|
try:
|
|
# Add project root to path
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from modules.plugin_loader import PluginLoader
|
|
from modules.db_manager import DBManager
|
|
from modules.utils import resolve_path
|
|
from modules.config_validation import strip_optional_quotes
|
|
except ImportError as e:
|
|
print("Error: Missing required dependencies.")
|
|
print(f"Details: {e}")
|
|
print("\nPlease install bot dependencies by running:")
|
|
print(" pip install -r requirements.txt")
|
|
print("\nOr install the missing module directly:")
|
|
if 'pytz' in str(e):
|
|
print(" pip install pytz")
|
|
sys.exit(1)
|
|
|
|
|
|
class MinimalBot:
|
|
"""Minimal bot mock for plugin loading without full bot initialization"""
|
|
|
|
def __init__(self, config, logger, db_manager=None):
|
|
self.config = config
|
|
self.logger = logger
|
|
self.db_manager = db_manager
|
|
|
|
# Dummy translator
|
|
class DummyTranslator:
|
|
def translate(self, key, **kwargs):
|
|
return key
|
|
def get_value(self, key):
|
|
return None
|
|
|
|
self.translator = DummyTranslator()
|
|
self.command_manager = None # Will be set after plugin loading
|
|
|
|
|
|
# Style definitions for website themes
|
|
# Each style contains CSS custom property values that will be injected into the :root selector
|
|
STYLES = {
|
|
'default': {
|
|
'name': 'Modern Dark',
|
|
'description': 'Dark theme with gradients and modern cards (current default)',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap',
|
|
'css_vars': """--bg-primary: #0a0e14;
|
|
--bg-secondary: #111820;
|
|
--bg-card: #151c25;
|
|
--bg-card-hover: #1a232e;
|
|
--accent-blue: #00d4ff;
|
|
--accent-cyan: #00ffc8;
|
|
--accent-orange: #ff8a00;
|
|
--accent-purple: #a855f7;
|
|
--accent-red: #ff4757;
|
|
--accent-yellow: #ffd700;
|
|
--text-primary: #e8edf4;
|
|
--text-secondary: #8892a4;
|
|
--text-muted: #6b7280;
|
|
--border-subtle: rgba(255,255,255,0.06);
|
|
--glow-blue: rgba(0, 212, 255, 0.15);
|
|
--glow-cyan: rgba(0, 255, 200, 0.1);"""
|
|
},
|
|
'minimalist': {
|
|
'name': 'Minimalist Clean',
|
|
'description': 'Light theme with clean typography and whitespace',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
|
|
'css_vars': """--bg-primary: #ffffff;
|
|
--bg-secondary: #ffffff;
|
|
--bg-card: #ffffff;
|
|
--bg-card-hover: #f8f9fa;
|
|
--accent-blue: #0052cc;
|
|
--accent-cyan: #006699;
|
|
--accent-orange: #d65d0e;
|
|
--accent-purple: #6b21a8;
|
|
--accent-red: #b91c1c;
|
|
--accent-yellow: #a16207;
|
|
--text-primary: #1a1a1a;
|
|
--text-secondary: #4a4a4a;
|
|
--text-muted: #6b6b6b;
|
|
--border-subtle: rgba(0,0,0,0.15);
|
|
--glow-blue: rgba(0,82,204,0.1);
|
|
--glow-cyan: rgba(0,102,153,0.08);""",
|
|
'css_overrides': """
|
|
/* Pure white background - remove atmospheric effects */
|
|
.atmosphere,
|
|
.grid-overlay {
|
|
display: none !important;
|
|
}
|
|
|
|
body {
|
|
background: #ffffff !important;
|
|
}
|
|
|
|
/* Ultra-minimal: square everything */
|
|
*,
|
|
*::before,
|
|
*::after,
|
|
.container,
|
|
.intro-section,
|
|
.sidebar-nav,
|
|
.sidebar-nav a,
|
|
.command-card,
|
|
.command-keyword,
|
|
.command-usage,
|
|
.channel-section,
|
|
.channel-card,
|
|
button,
|
|
input {
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
/* Clean Inter font everywhere */
|
|
body,
|
|
h1, h2, h3, h4, h5, h6,
|
|
p, a, span, div {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
|
|
}
|
|
|
|
/* Remove all shadows */
|
|
.command-card,
|
|
.intro-section,
|
|
.sidebar-nav,
|
|
.command-keyword,
|
|
.command-usage {
|
|
box-shadow: none !important;
|
|
}
|
|
|
|
/* Thin, clean borders */
|
|
.command-card {
|
|
border: 1px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
.intro-section {
|
|
border: 1px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
.sidebar-nav {
|
|
border-right: 1px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
.command-keyword {
|
|
border: 1px solid currentColor !important;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.command-usage {
|
|
border-left: 2px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
/* Minimal hover effects - just background color */
|
|
.command-card:hover {
|
|
box-shadow: none !important;
|
|
transform: none !important;
|
|
}
|
|
|
|
.sidebar-nav a:hover {
|
|
background: var(--bg-card-hover) !important;
|
|
}
|
|
|
|
/* Typography: lighter weight, more spacing */
|
|
h1 {
|
|
font-weight: 600;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
h2, h3 {
|
|
font-weight: 500;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.command-name {
|
|
font-weight: 600;
|
|
}
|
|
|
|
p, .command-description {
|
|
font-weight: 400;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* Remove all animations and transitions for instant feedback */
|
|
*, *::before, *::after {
|
|
animation: none !important;
|
|
transition: background-color 0.1s ease !important;
|
|
}
|
|
|
|
/* Clean spacing */
|
|
.command-card {
|
|
padding: 1.5rem !important;
|
|
}
|
|
|
|
/* Subtle, clean keyword badges */
|
|
.command-keyword {
|
|
padding: 0.25rem 0.5rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
"""
|
|
},
|
|
'terminal': {
|
|
'name': 'Terminal/Hacker',
|
|
'description': 'Green/amber on black, monospace, retro terminal aesthetic',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap',
|
|
'css_vars': """--bg-primary: #000000;
|
|
--bg-secondary: #0a0a0a;
|
|
--bg-card: #0f0f0f;
|
|
--bg-card-hover: #1a1a1a;
|
|
--accent-blue: #00ff00;
|
|
--accent-cyan: #00ff00;
|
|
--accent-orange: #ffb000;
|
|
--accent-purple: #00ff00;
|
|
--accent-red: #ff0000;
|
|
--accent-yellow: #ffff00;
|
|
--text-primary: #00ff00;
|
|
--text-secondary: #00aa00;
|
|
--text-muted: #008800;
|
|
--border-subtle: rgba(0,255,0,0.3);
|
|
--glow-blue: rgba(0,255,0,0.2);
|
|
--glow-cyan: rgba(0,255,0,0.15);""",
|
|
'css_overrides': """
|
|
/* Terminal monospace font everywhere */
|
|
body,
|
|
h1, h2, h3, h4, h5, h6,
|
|
p, a, span, div,
|
|
.command-name,
|
|
.command-description,
|
|
.command-usage,
|
|
.command-keyword,
|
|
.sidebar-nav a {
|
|
font-family: 'JetBrains Mono', 'Courier New', monospace !important;
|
|
}
|
|
|
|
/* CRT scanline effect */
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: repeating-linear-gradient(
|
|
0deg,
|
|
rgba(0, 255, 0, 0.03),
|
|
rgba(0, 255, 0, 0.03) 1px,
|
|
transparent 1px,
|
|
transparent 2px
|
|
);
|
|
pointer-events: none;
|
|
z-index: 9999;
|
|
}
|
|
|
|
/* Terminal text glow */
|
|
h1, h2, h3,
|
|
.command-name {
|
|
text-shadow: 0 0 10px rgba(0, 255, 0, 0.8);
|
|
}
|
|
|
|
.command-keyword,
|
|
.command-usage {
|
|
text-shadow: 0 0 5px rgba(0, 255, 0, 0.6);
|
|
}
|
|
|
|
/* Cursor blink effect after usage example */
|
|
.command-card:hover .command-usage::after {
|
|
content: '▮';
|
|
display: inline-block;
|
|
margin-left: 0.5rem;
|
|
animation: blink 1s step-end infinite;
|
|
color: var(--accent-blue);
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 50% { opacity: 1; }
|
|
51%, 100% { opacity: 0; }
|
|
}
|
|
|
|
/* Terminal prompt style for usage */
|
|
.command-usage::before {
|
|
content: '$ ';
|
|
color: var(--accent-yellow);
|
|
font-weight: bold;
|
|
}
|
|
"""
|
|
},
|
|
'glass': {
|
|
'name': 'Glass/Glassmorphism',
|
|
'description': 'Frosted glass cards with blur effects and colorful gradients',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap',
|
|
'css_vars': """--bg-primary: linear-gradient(135deg, #4c5fd7 0%, #764ba2 100%);
|
|
--bg-secondary: rgba(255,255,255,0.05);
|
|
--bg-card: rgba(255,255,255,0.1);
|
|
--bg-card-hover: rgba(255,255,255,0.15);
|
|
--accent-blue: #a8daff;
|
|
--accent-cyan: #a8fff4;
|
|
--accent-orange: #ffc085;
|
|
--accent-purple: #d4b3ff;
|
|
--accent-red: #ff9eb5;
|
|
--accent-yellow: #ffe8a8;
|
|
--text-primary: #ffffff;
|
|
--text-secondary: rgba(255,255,255,0.8);
|
|
--text-muted: rgba(255,255,255,0.6);
|
|
--border-subtle: rgba(255,255,255,0.2);
|
|
--glow-blue: rgba(168,218,255,0.2);
|
|
--glow-cyan: rgba(168,255,244,0.15);"""
|
|
},
|
|
'neon': {
|
|
'name': 'Neon/Cyberpunk',
|
|
'description': 'Bright neon colors, dark backgrounds, futuristic aesthetic',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&display=swap',
|
|
'css_vars': """--bg-primary: #0a0014;
|
|
--bg-secondary: #150028;
|
|
--bg-card: #1a0033;
|
|
--bg-card-hover: #25004d;
|
|
--accent-blue: #00f5ff;
|
|
--accent-cyan: #00f5ff;
|
|
--accent-orange: #ff69b4;
|
|
--accent-purple: #bf40bf;
|
|
--accent-red: #ff1493;
|
|
--accent-yellow: #ffd700;
|
|
--text-primary: #ffffff;
|
|
--text-secondary: #e9d5ff;
|
|
--text-muted: #d8b4fe;
|
|
--border-subtle: rgba(157,78,221,0.3);
|
|
--glow-blue: rgba(0,245,255,0.4);
|
|
--glow-cyan: rgba(247,37,133,0.3);""",
|
|
'css_overrides': """
|
|
/* Neon cyberpunk font */
|
|
body,
|
|
h1, h2, h3, h4, h5, h6,
|
|
.command-name,
|
|
.sidebar-nav a {
|
|
font-family: 'Orbitron', sans-serif !important;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Intense neon glow on headings */
|
|
h1 {
|
|
text-shadow:
|
|
0 0 10px var(--accent-cyan),
|
|
0 0 20px var(--accent-cyan),
|
|
0 0 30px var(--accent-cyan),
|
|
0 0 40px var(--accent-blue);
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
h2, h3 {
|
|
text-shadow:
|
|
0 0 10px var(--accent-purple),
|
|
0 0 20px var(--accent-purple);
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.command-name {
|
|
text-shadow:
|
|
0 0 10px var(--accent-cyan),
|
|
0 0 20px var(--accent-cyan);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
/* Neon border glow on cards - subtle */
|
|
.command-card {
|
|
border: 2px solid var(--accent-cyan);
|
|
box-shadow:
|
|
0 0 5px var(--glow-blue),
|
|
inset 0 0 5px rgba(0,245,255,0.05);
|
|
}
|
|
|
|
.command-card:hover {
|
|
border-color: var(--accent-orange);
|
|
box-shadow:
|
|
0 0 10px var(--accent-cyan),
|
|
0 0 20px var(--accent-orange),
|
|
inset 0 0 10px rgba(255,105,180,0.1);
|
|
transform: translateY(-4px);
|
|
}
|
|
|
|
.intro-section {
|
|
border: 2px solid var(--accent-purple);
|
|
box-shadow:
|
|
0 0 8px var(--accent-purple),
|
|
inset 0 0 8px rgba(191,64,191,0.05);
|
|
}
|
|
|
|
/* Neon keyword badges */
|
|
.command-keyword {
|
|
text-shadow: 0 0 8px currentColor;
|
|
border: 1px solid currentColor;
|
|
box-shadow:
|
|
0 0 5px currentColor,
|
|
inset 0 0 5px currentColor;
|
|
}
|
|
|
|
/* Glowing usage examples */
|
|
.command-usage {
|
|
border-left: 3px solid var(--accent-cyan);
|
|
box-shadow: -3px 0 10px var(--glow-blue);
|
|
text-shadow: 0 0 5px rgba(0,245,255,0.5);
|
|
}
|
|
|
|
/* Animated neon flicker effect */
|
|
@keyframes neon-flicker {
|
|
0%, 100% { opacity: 1; }
|
|
41%, 43% { opacity: 0.8; }
|
|
45%, 47% { opacity: 0.9; }
|
|
49% { opacity: 0.85; }
|
|
}
|
|
|
|
h1, .command-name {
|
|
animation: neon-flicker 3s infinite;
|
|
}
|
|
|
|
/* Cyberpunk grid overlay */
|
|
body::after {
|
|
content: '';
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-image:
|
|
linear-gradient(rgba(0,245,255,0.03) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(0,245,255,0.03) 1px, transparent 1px);
|
|
background-size: 50px 50px;
|
|
pointer-events: none;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* Ensure content is above grid */
|
|
.container {
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
/* Sidebar neon accent */
|
|
.sidebar-nav {
|
|
border-right: 2px solid var(--accent-cyan);
|
|
box-shadow: 2px 0 15px var(--glow-blue);
|
|
}
|
|
|
|
.sidebar-nav a:hover {
|
|
text-shadow: 0 0 10px var(--accent-cyan);
|
|
background: rgba(0,245,255,0.1);
|
|
}
|
|
"""
|
|
},
|
|
'brutalist': {
|
|
'name': 'Brutalist/Bold',
|
|
'description': 'High contrast, bold typography, thick borders, geometric',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700;900&display=swap',
|
|
'css_vars': """--bg-primary: #ffffff;
|
|
--bg-secondary: #f0f0f0;
|
|
--bg-card: #ffffff;
|
|
--bg-card-hover: #f5f5f5;
|
|
--accent-blue: #0000cc;
|
|
--accent-cyan: #006666;
|
|
--accent-orange: #cc5200;
|
|
--accent-purple: #5c00b8;
|
|
--accent-red: #cc0000;
|
|
--accent-yellow: #997700;
|
|
--text-primary: #000000;
|
|
--text-secondary: #1a1a1a;
|
|
--text-muted: #4d4d4d;
|
|
--border-subtle: rgba(0,0,0,1);
|
|
--glow-blue: rgba(0,0,204,0.2);
|
|
--glow-cyan: rgba(0,102,102,0.2);""",
|
|
'css_overrides': """
|
|
/* BRUTALIST: Square everything aggressively */
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
/* Space Grotesk everywhere, heavy weights */
|
|
body,
|
|
h1, h2, h3, h4, h5, h6,
|
|
p, a, span, div {
|
|
font-family: 'Space Grotesk', sans-serif !important;
|
|
}
|
|
|
|
/* MASSIVE, BOLD headings */
|
|
h1 {
|
|
font-weight: 900 !important;
|
|
font-size: 4rem !important;
|
|
letter-spacing: -0.05em;
|
|
text-transform: uppercase;
|
|
line-height: 0.9;
|
|
}
|
|
|
|
.category-title {
|
|
font-weight: 900 !important;
|
|
font-size: 2.5rem !important;
|
|
text-transform: uppercase;
|
|
letter-spacing: -0.03em;
|
|
border-bottom: 8px solid #000000 !important;
|
|
padding-bottom: 0.5rem !important;
|
|
}
|
|
|
|
.category-title .anchor-link {
|
|
text-decoration: none;
|
|
color: #000000;
|
|
}
|
|
|
|
.command-name {
|
|
font-weight: 900 !important;
|
|
font-size: 1.5rem !important;
|
|
text-transform: uppercase;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
/* THICK borders everywhere */
|
|
.command-card {
|
|
border: 6px solid #000000 !important;
|
|
box-shadow: 12px 12px 0 #000000 !important;
|
|
}
|
|
|
|
.command-card:hover {
|
|
box-shadow: 16px 16px 0 #000000 !important;
|
|
transform: translate(-4px, -4px) !important;
|
|
}
|
|
|
|
/* Brutal intro box */
|
|
.header-content {
|
|
border: 6px solid #000000 !important;
|
|
box-shadow: 12px 12px 0 #000000 !important;
|
|
border-radius: 0 !important;
|
|
background: #ffffff !important;
|
|
}
|
|
|
|
.header-content::before {
|
|
display: none !important;
|
|
}
|
|
|
|
header {
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
/* Bold, chunky keyword badges */
|
|
.command-keyword {
|
|
font-weight: 700 !important;
|
|
border: 3px solid #000000 !important;
|
|
padding: 0.5rem 1rem !important;
|
|
background: #ffffff;
|
|
text-transform: uppercase;
|
|
font-size: 0.75rem !important;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.command-keyword:nth-child(odd) {
|
|
background: #000000;
|
|
color: #ffffff;
|
|
}
|
|
|
|
/* Heavy usage box */
|
|
.command-usage {
|
|
border: 4px solid #000000 !important;
|
|
background: #f0f0f0 !important;
|
|
padding: 1rem !important;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* Aggressive sidebar - SQUARE */
|
|
.sidebar-nav {
|
|
border-right: 8px solid #000000 !important;
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
.sidebar-nav a {
|
|
font-weight: 700 !important;
|
|
text-transform: uppercase;
|
|
font-size: 0.85rem !important;
|
|
letter-spacing: 0.02em;
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
.sidebar-nav a:hover {
|
|
background: #000000 !important;
|
|
color: #ffffff !important;
|
|
}
|
|
|
|
/* Remove blue hover bar on command cards */
|
|
.command-card::before {
|
|
display: none !important;
|
|
}
|
|
|
|
/* CHANNEL CARDS - same brutal treatment */
|
|
.channel-card {
|
|
border: 6px solid #000000 !important;
|
|
box-shadow: 12px 12px 0 #000000 !important;
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
.channel-card:hover {
|
|
box-shadow: 16px 16px 0 #000000 !important;
|
|
transform: translate(-4px, -4px) !important;
|
|
}
|
|
|
|
.channel-name {
|
|
font-weight: 900 !important;
|
|
font-size: 1.5rem !important;
|
|
text-transform: uppercase;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.channel-description {
|
|
font-weight: 500 !important;
|
|
}
|
|
|
|
.channel-section h2 {
|
|
font-weight: 900 !important;
|
|
font-size: 2.5rem !important;
|
|
text-transform: uppercase;
|
|
letter-spacing: -0.03em;
|
|
border-bottom: 8px solid #000000 !important;
|
|
padding-bottom: 0.5rem !important;
|
|
}
|
|
|
|
/* Remove all smooth transitions - instant feedback */
|
|
* {
|
|
transition: none !important;
|
|
}
|
|
|
|
/* Heavy typography throughout */
|
|
p, .command-description {
|
|
font-weight: 500 !important;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Brutal intro section */
|
|
.intro {
|
|
font-weight: 700 !important;
|
|
font-size: 1.2rem !important;
|
|
}
|
|
"""
|
|
},
|
|
'gradient': {
|
|
'name': 'Gradient/Modern',
|
|
'description': 'Colorful gradients, smooth transitions, vibrant colors',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap',
|
|
'css_vars': """--bg-primary: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
|
--bg-secondary: rgba(0,0,0,0.08);
|
|
--bg-card: rgba(255,255,255,0.95);
|
|
--bg-card-hover: rgba(255,255,255,0.98);
|
|
--accent-blue: #4338ca;
|
|
--accent-cyan: #0891b2;
|
|
--accent-orange: #ea580c;
|
|
--accent-purple: #7c3aed;
|
|
--accent-red: #dc2626;
|
|
--accent-yellow: #ca8a04;
|
|
--text-primary: #1a1a1a;
|
|
--text-secondary: #374151;
|
|
--text-muted: #6b7280;
|
|
--border-subtle: rgba(0,0,0,0.15);
|
|
--glow-blue: rgba(79,70,229,0.3);
|
|
--glow-cyan: rgba(8,145,178,0.25);""",
|
|
'css_overrides': """
|
|
/* Vibrant gradient borders */
|
|
.command-card {
|
|
border: 2px solid transparent;
|
|
background:
|
|
linear-gradient(white, white) padding-box,
|
|
linear-gradient(135deg, #667eea, #764ba2, #f093fb) border-box;
|
|
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.15);
|
|
background-origin: border-box;
|
|
background-clip: padding-box, border-box;
|
|
}
|
|
|
|
.command-card:hover {
|
|
box-shadow: 0 8px 30px rgba(102, 126, 234, 0.25), 0 0 0 1px rgba(102, 126, 234, 0.1);
|
|
transform: translateY(-4px);
|
|
}
|
|
|
|
.intro-section {
|
|
border: 2px solid transparent;
|
|
background:
|
|
linear-gradient(white, white) padding-box,
|
|
linear-gradient(135deg, #667eea, #00d4ff) border-box;
|
|
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.15);
|
|
}
|
|
|
|
/* Gradient text for headings */
|
|
h1 {
|
|
background: linear-gradient(135deg, #667eea, #764ba2, #f093fb);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
/* Keep section headers (category titles) visible with high contrast */
|
|
.category-title .anchor-link {
|
|
background: linear-gradient(135deg, #1a1a1a, #374151);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
text-decoration: none;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.command-name {
|
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* Colorful keyword badges with gradients */
|
|
.command-keyword {
|
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
color: white;
|
|
border: none;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.command-keyword:nth-child(2) {
|
|
background: linear-gradient(135deg, #f093fb, #667eea);
|
|
}
|
|
|
|
.command-keyword:nth-child(3) {
|
|
background: linear-gradient(135deg, #764ba2, #f093fb);
|
|
}
|
|
|
|
.command-keyword:nth-child(4) {
|
|
background: linear-gradient(135deg, #00d4ff, #667eea);
|
|
}
|
|
|
|
.command-keyword:nth-child(5) {
|
|
background: linear-gradient(135deg, #f093fb, #00d4ff);
|
|
}
|
|
|
|
/* Animated gradient on hover */
|
|
@keyframes gradient-shift {
|
|
0% { background-position: 0% 50%; }
|
|
50% { background-position: 100% 50%; }
|
|
100% { background-position: 0% 50%; }
|
|
}
|
|
|
|
.command-card:hover .command-name {
|
|
background: linear-gradient(135deg, #667eea, #764ba2, #f093fb, #00d4ff);
|
|
background-size: 300% 300%;
|
|
animation: gradient-shift 3s ease infinite;
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
/* Gradient usage box */
|
|
.command-usage {
|
|
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05), rgba(240, 147, 251, 0.05));
|
|
border-left: 3px solid;
|
|
border-image: linear-gradient(135deg, #667eea, #f093fb) 1;
|
|
}
|
|
|
|
/* Sidebar gradient accent */
|
|
.sidebar-nav {
|
|
border-right: 3px solid transparent;
|
|
border-image: linear-gradient(180deg, #667eea, #764ba2, #f093fb) 1;
|
|
}
|
|
|
|
.sidebar-nav a:hover {
|
|
background: linear-gradient(90deg, rgba(102, 126, 234, 0.1), transparent);
|
|
border-left: 3px solid #667eea;
|
|
padding-left: 1.5rem;
|
|
}
|
|
"""
|
|
},
|
|
'pixel': {
|
|
'name': 'Pixel/Retro',
|
|
'description': 'Pixel art aesthetic, squared boxes, bright retro gaming colors',
|
|
'fonts_url': 'https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap',
|
|
'css_vars': """--bg-primary: #2b2d42;
|
|
--bg-secondary: #3a3d5c;
|
|
--bg-card: #4a4e69;
|
|
--bg-card-hover: #5a5f7e;
|
|
--accent-blue: #00b4d8;
|
|
--accent-cyan: #00f5d4;
|
|
--accent-orange: #ff6b35;
|
|
--accent-purple: #b185db;
|
|
--accent-red: #ef476f;
|
|
--accent-yellow: #ffd60a;
|
|
--text-primary: #ffffff;
|
|
--text-secondary: #e0e0e0;
|
|
--text-muted: #a8a8a8;
|
|
--border-subtle: rgba(255,255,255,0.3);
|
|
--glow-blue: rgba(0,180,216,0.4);
|
|
--glow-cyan: rgba(0,245,212,0.3);""",
|
|
'css_overrides': """
|
|
/* Pixel art: remove ALL rounded corners */
|
|
*,
|
|
*::before,
|
|
*::after,
|
|
.container,
|
|
.intro-section,
|
|
.sidebar-nav,
|
|
.sidebar-nav a,
|
|
.command-card,
|
|
.command-keyword,
|
|
.command-usage,
|
|
.channel-section,
|
|
.channel-card,
|
|
.mobile-menu-toggle,
|
|
button,
|
|
input {
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
/* Chunky pixel borders */
|
|
.command-card {
|
|
border: 3px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
.intro-section {
|
|
border: 3px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
.sidebar-nav {
|
|
border: 3px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
.command-keyword {
|
|
border: 2px solid currentColor !important;
|
|
}
|
|
|
|
.command-usage {
|
|
border: 2px solid var(--accent-cyan) !important;
|
|
}
|
|
|
|
.channel-section {
|
|
border: 3px solid var(--border-subtle) !important;
|
|
}
|
|
|
|
/* Pixel font sizing adjustments */
|
|
body {
|
|
font-family: 'Press Start 2P', cursive !important;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.5rem !important;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 1.2rem !important;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
h3 {
|
|
font-size: 1rem !important;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.command-name {
|
|
font-size: 1.1rem !important;
|
|
}
|
|
|
|
p, .command-description, .command-usage {
|
|
font-size: 0.75rem !important;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
.command-keyword {
|
|
font-size: 0.65rem !important;
|
|
padding: 4px 8px;
|
|
}
|
|
|
|
.sidebar-nav a {
|
|
font-size: 0.7rem !important;
|
|
}
|
|
|
|
/* Retro box shadow - hard edges */
|
|
.command-card {
|
|
box-shadow: 4px 4px 0 var(--border-subtle) !important;
|
|
}
|
|
|
|
.command-card:hover {
|
|
box-shadow: 8px 8px 0 var(--accent-cyan) !important;
|
|
transform: translate(-2px, -2px) !important;
|
|
}
|
|
|
|
.intro-section {
|
|
box-shadow: 4px 4px 0 var(--border-subtle) !important;
|
|
}
|
|
|
|
/* Remove smooth transitions for pixel feel */
|
|
* {
|
|
transition: none !important;
|
|
}
|
|
"""
|
|
}
|
|
}
|
|
|
|
|
|
def setup_logging():
|
|
"""Setup basic logging"""
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(levelname)s: %(message)s'
|
|
)
|
|
return logging.getLogger(__name__)
|
|
|
|
|
|
def read_config(config_file: str = "config.ini") -> configparser.ConfigParser:
|
|
"""Read and parse config.ini file"""
|
|
config = configparser.ConfigParser()
|
|
if not os.path.exists(config_file):
|
|
raise FileNotFoundError(f"Config file not found: {config_file}")
|
|
config.read(config_file)
|
|
return config
|
|
|
|
|
|
def get_bot_name(config: configparser.ConfigParser) -> str:
|
|
"""Extract bot name from config"""
|
|
return config.get('Bot', 'bot_name', fallback='MeshCore Bot')
|
|
|
|
|
|
def get_admin_commands(config: configparser.ConfigParser) -> List[str]:
|
|
"""Extract admin commands from config"""
|
|
if not config.has_section('Admin_ACL'):
|
|
return []
|
|
admin_commands_str = config.get('Admin_ACL', 'admin_commands', fallback='')
|
|
if not admin_commands_str:
|
|
return []
|
|
return [cmd.strip() for cmd in admin_commands_str.split(',') if cmd.strip()]
|
|
|
|
|
|
def get_website_intro(config: configparser.ConfigParser) -> str:
|
|
"""Get custom introduction text from config, or use default"""
|
|
if config.has_section('Website'):
|
|
intro = config.get('Website', 'introduction_text', fallback='')
|
|
if intro:
|
|
return intro
|
|
|
|
# Default introduction (first person from bot's perspective)
|
|
bot_name = get_bot_name(config)
|
|
return f"Hi, I'm {bot_name}! I provide various commands to help you interact with the mesh network. Use the commands below to get started."
|
|
|
|
|
|
def get_website_title(config: configparser.ConfigParser) -> str:
|
|
"""Get website title from config, or use default"""
|
|
if config.has_section('Website'):
|
|
title = config.get('Website', 'website_title', fallback='')
|
|
if title:
|
|
return title
|
|
|
|
bot_name = get_bot_name(config)
|
|
return f"{bot_name} - Command Reference"
|
|
|
|
|
|
def load_channels_from_config(config: configparser.ConfigParser) -> Dict[str, Dict[str, str]]:
|
|
"""Load channels from Channels_List section, grouped by category
|
|
|
|
Returns:
|
|
Dict with structure: {
|
|
'general': {'#channel': 'description', ...},
|
|
'category': {'#channel': 'description', ...},
|
|
...
|
|
}
|
|
"""
|
|
channels = {'general': {}}
|
|
|
|
if not config.has_section('Channels_List'):
|
|
return channels
|
|
|
|
for key, description in config.items('Channels_List'):
|
|
key = key.strip()
|
|
description = description.strip()
|
|
|
|
if not key or not description:
|
|
continue
|
|
|
|
# Check if it's a categorized channel (has dot notation)
|
|
if '.' in key:
|
|
category = key.split('.')[0]
|
|
channel_name = key.split('.', 1)[1]
|
|
|
|
if category not in channels:
|
|
channels[category] = {}
|
|
|
|
# Add # prefix if not present
|
|
display_name = channel_name if channel_name.startswith('#') else f"#{channel_name}"
|
|
channels[category][display_name] = description
|
|
else:
|
|
# General channel (no category)
|
|
display_name = key if key.startswith('#') else f"#{key}"
|
|
channels['general'][display_name] = description
|
|
|
|
# Remove empty categories
|
|
return {cat: chans for cat, chans in channels.items() if chans}
|
|
|
|
|
|
def get_database_path(config: configparser.ConfigParser, bot_root: str) -> Optional[str]:
|
|
"""Get database path from config"""
|
|
db_path = config.get('Bot', 'db_path', fallback='meshcore_bot.db')
|
|
if db_path:
|
|
return resolve_path(db_path, bot_root)
|
|
return None
|
|
|
|
|
|
def get_command_popularity(db_path: Optional[str], commands: Dict[str, Any]) -> Dict[str, int]:
|
|
"""Get command usage counts from database"""
|
|
popularity = defaultdict(int)
|
|
|
|
if not db_path or not os.path.exists(db_path):
|
|
return popularity
|
|
|
|
try:
|
|
with closing(sqlite3.connect(db_path)) as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Check if command_stats table exists
|
|
cursor.execute("""
|
|
SELECT name FROM sqlite_master
|
|
WHERE type='table' AND name='command_stats'
|
|
""")
|
|
if not cursor.fetchone():
|
|
return popularity
|
|
|
|
# Query command usage
|
|
cursor.execute("""
|
|
SELECT command_name, COUNT(*) as count
|
|
FROM command_stats
|
|
GROUP BY command_name
|
|
""")
|
|
|
|
for row in cursor.fetchall():
|
|
command_name = row[0]
|
|
count = row[1]
|
|
|
|
# Map to primary command name if it's a keyword/alias
|
|
primary_name = command_name
|
|
for cmd_name, cmd_instance in commands.items():
|
|
if hasattr(cmd_instance, 'keywords') and command_name.lower() in [k.lower() for k in cmd_instance.keywords]:
|
|
primary_name = cmd_instance.name if hasattr(cmd_instance, 'name') else cmd_name
|
|
break
|
|
if hasattr(cmd_instance, 'name') and cmd_instance.name == command_name:
|
|
primary_name = cmd_instance.name
|
|
break
|
|
|
|
popularity[primary_name] += count
|
|
except Exception as e:
|
|
logging.warning(f"Could not query command popularity: {e}")
|
|
|
|
return popularity
|
|
|
|
|
|
def filter_commands(commands: Dict[str, Any], admin_commands: List[str]) -> Dict[str, Any]:
|
|
"""Filter out admin and hidden commands"""
|
|
filtered = {}
|
|
|
|
# Categories to exclude from public reference
|
|
excluded_categories = {'hidden', 'admin', 'system', 'management', 'special'}
|
|
|
|
for cmd_name, cmd_instance in commands.items():
|
|
# Skip commands in excluded categories
|
|
if hasattr(cmd_instance, 'category') and cmd_instance.category in excluded_categories:
|
|
continue
|
|
|
|
# Get primary command name
|
|
primary_name = cmd_instance.name if hasattr(cmd_instance, 'name') else cmd_name
|
|
|
|
# Skip admin commands by name (check both dict key and primary name)
|
|
if cmd_name in admin_commands or primary_name in admin_commands:
|
|
continue
|
|
|
|
# Skip commands that require admin access
|
|
if hasattr(cmd_instance, 'requires_admin_access') and cmd_instance.requires_admin_access():
|
|
continue
|
|
|
|
# Skip commands with no keywords (automatic/system commands)
|
|
if hasattr(cmd_instance, 'keywords') and not cmd_instance.keywords:
|
|
continue
|
|
|
|
filtered[cmd_name] = cmd_instance
|
|
|
|
return filtered
|
|
|
|
|
|
def get_default_command_order() -> List[str]:
|
|
"""Get default command ordering when no stats available"""
|
|
return [
|
|
'help', 'ping', 'test', # Basic
|
|
'wx', 'aqi', # Weather
|
|
'sun', 'moon', 'solar', # Solar
|
|
'sports', 'stats', # Popular features
|
|
]
|
|
|
|
|
|
def sort_commands_by_popularity(commands: Dict[str, Any], popularity: Dict[str, int]) -> List[Tuple[str, Any]]:
|
|
"""Sort commands by popularity, with fallback to default order"""
|
|
default_order = get_default_command_order()
|
|
|
|
# Create list of (name, instance, priority) tuples
|
|
command_list = []
|
|
for cmd_name, cmd_instance in commands.items():
|
|
primary_name = cmd_instance.name if hasattr(cmd_instance, 'name') else cmd_name
|
|
|
|
# Get popularity count
|
|
count = popularity.get(primary_name, 0)
|
|
|
|
# Get default priority (lower is better)
|
|
try:
|
|
default_priority = default_order.index(primary_name)
|
|
except ValueError:
|
|
default_priority = 999 # Not in default order
|
|
|
|
# Priority: popularity count (descending), then default order, then alphabetical
|
|
priority = (-count, default_priority, primary_name.lower())
|
|
command_list.append((priority, cmd_name, cmd_instance))
|
|
|
|
# Sort by priority
|
|
command_list.sort(key=lambda x: x[0])
|
|
|
|
# Return (name, instance) tuples
|
|
return [(name, instance) for _, name, instance in command_list]
|
|
|
|
|
|
def escape_html(text: str) -> str:
|
|
"""Escape HTML special characters"""
|
|
return html.escape(str(text))
|
|
|
|
|
|
def get_channel_info(cmd_instance: Any, monitor_channels: List[str]) -> Optional[str]:
|
|
"""Get channel restriction information for a command"""
|
|
allowed_channels = getattr(cmd_instance, 'allowed_channels', None)
|
|
requires_dm = getattr(cmd_instance, 'requires_dm', False)
|
|
|
|
# If command has specific allowed channels configured
|
|
if allowed_channels is not None:
|
|
if allowed_channels == []:
|
|
# Empty list means DM only (channels explicitly disabled)
|
|
return "DM only"
|
|
elif len(allowed_channels) > 0:
|
|
# Format channel names (add # if not present)
|
|
formatted_channels = []
|
|
for ch in allowed_channels:
|
|
ch = ch.strip()
|
|
if not ch.startswith('#'):
|
|
formatted_channels.append(f"#{ch}")
|
|
else:
|
|
formatted_channels.append(ch)
|
|
if len(formatted_channels) == 1:
|
|
return f"Channel: {', '.join(formatted_channels)}"
|
|
else:
|
|
return f"Channels: {', '.join(formatted_channels)}"
|
|
|
|
# If command requires DM and has no channel override, it's DM only
|
|
if requires_dm:
|
|
return "DM only"
|
|
|
|
# No restrictions - works in all monitored channels (don't show anything)
|
|
return None
|
|
|
|
|
|
def format_monitor_channels(monitor_channels: List[str], html: bool = False) -> str:
|
|
"""Format monitor channels for display
|
|
|
|
Args:
|
|
monitor_channels: List of channel names
|
|
html: If True, wrap channel names in span tags for highlighting
|
|
"""
|
|
if not monitor_channels:
|
|
return ""
|
|
|
|
formatted = []
|
|
for ch in monitor_channels:
|
|
ch = ch.strip()
|
|
if not ch.startswith('#'):
|
|
channel_name = f"#{ch}"
|
|
else:
|
|
channel_name = ch
|
|
|
|
if html:
|
|
# Wrap in span for inline highlighting
|
|
formatted.append(f'<span class="channel-highlight">{escape_html(channel_name)}</span>')
|
|
else:
|
|
formatted.append(channel_name)
|
|
|
|
if len(formatted) == 1:
|
|
return formatted[0]
|
|
elif len(formatted) == 2:
|
|
return f"{formatted[0]} or {formatted[1]}"
|
|
else:
|
|
return ", ".join(formatted[:-1]) + f", or {formatted[-1]}"
|
|
|
|
|
|
def generate_html(bot_name: str, title: str, introduction: str, commands: List[Tuple[str, Any]], monitor_channels: List[str] = None, channels_data: Dict[str, Dict[str, str]] = None, style: str = 'default') -> str:
|
|
"""Generate the HTML content"""
|
|
|
|
if monitor_channels is None:
|
|
monitor_channels = []
|
|
|
|
if channels_data is None:
|
|
channels_data = {}
|
|
|
|
# Format monitor channels message to append to introduction
|
|
channels_suffix = ""
|
|
if monitor_channels:
|
|
formatted_channels = format_monitor_channels(monitor_channels, html=True)
|
|
channels_suffix = f" I'll answer you if you send a message in {formatted_channels}."
|
|
|
|
# Group commands by category
|
|
categories = defaultdict(list)
|
|
for cmd_name, cmd_instance in commands:
|
|
category = getattr(cmd_instance, 'category', 'general')
|
|
categories[category].append((cmd_name, cmd_instance))
|
|
|
|
# Category display names
|
|
category_names = {
|
|
'basic': 'Basic Commands',
|
|
'weather': 'Weather Commands',
|
|
'solar': 'Solar & Astronomical',
|
|
'sports': 'Sports',
|
|
'games': 'Games & Entertainment',
|
|
'fun': 'Fun Commands',
|
|
'entertainment': 'Entertainment',
|
|
'meshcore_info': 'Mesh Network Info',
|
|
'analytics': 'Analytics',
|
|
'emergency': 'Emergency',
|
|
'special': 'Special Commands',
|
|
'general': 'General Commands',
|
|
}
|
|
|
|
# Build navigation sidebar items
|
|
nav_items = []
|
|
|
|
# Add command categories to nav (basic first, then alphabetically)
|
|
sorted_categories = sorted(categories.keys())
|
|
if 'basic' in sorted_categories:
|
|
sorted_categories.remove('basic')
|
|
sorted_categories.insert(0, 'basic')
|
|
|
|
for category in sorted_categories:
|
|
category_display = category_names.get(category, category.title().replace('_', ' '))
|
|
category_id = category.lower().replace('_', '-').replace(' ', '-')
|
|
nav_items.append(('commands', category_display, f'commands-{category_id}'))
|
|
|
|
# Add channels section to nav if available
|
|
if channels_data:
|
|
nav_items.append(('channels', 'Available Channels', 'channels'))
|
|
|
|
# Add channel subcategories
|
|
sorted_channel_categories = ['general'] + sorted([c for c in channels_data.keys() if c != 'general'])
|
|
for category in sorted_channel_categories:
|
|
if category not in channels_data or not channels_data[category]:
|
|
continue
|
|
category_display = category.title().replace('_', ' ') if category != 'general' else 'General Channels'
|
|
category_id = category.lower().replace('_', '-').replace(' ', '-')
|
|
nav_items.append(('channels-sub', category_display, f'channels-{category_id}'))
|
|
|
|
# Build navigation sidebar HTML
|
|
nav_html = '<nav class="sidebar-nav">\n'
|
|
nav_html += ' <div class="nav-header">\n'
|
|
nav_html += ' <h3>Navigation</h3>\n'
|
|
nav_html += ' </div>\n'
|
|
nav_html += ' <ul class="nav-list">\n'
|
|
|
|
# Add Commands section
|
|
if nav_items:
|
|
nav_html += ' <li class="nav-section-header">Commands</li>\n'
|
|
nav_html += ' <ul class="nav-sublist">\n'
|
|
|
|
for nav_type, display_name, anchor_id in nav_items:
|
|
if nav_type == 'commands':
|
|
nav_html += f' <li><a href="#{anchor_id}" class="nav-link nav-sublink">{escape_html(display_name)}</a></li>\n'
|
|
elif nav_type == 'channels':
|
|
# Close commands section and start channels
|
|
nav_html += ' </ul>\n'
|
|
nav_html += ' <li class="nav-section-header">Channels</li>\n'
|
|
nav_html += ' <ul class="nav-sublist">\n'
|
|
nav_html += f' <li><a href="#{anchor_id}" class="nav-link nav-sublink">{escape_html(display_name)}</a></li>\n'
|
|
elif nav_type == 'channels-sub':
|
|
nav_html += f' <li><a href="#{anchor_id}" class="nav-link nav-sublink">{escape_html(display_name)}</a></li>\n'
|
|
|
|
nav_html += ' </ul>\n'
|
|
|
|
nav_html += ' </ul>\n'
|
|
nav_html += '</nav>\n'
|
|
|
|
# Build command HTML (basic first, then alphabetically)
|
|
commands_html = ""
|
|
sorted_command_categories = sorted(categories.keys())
|
|
if 'basic' in sorted_command_categories:
|
|
sorted_command_categories.remove('basic')
|
|
sorted_command_categories.insert(0, 'basic')
|
|
|
|
for category in sorted_command_categories:
|
|
category_commands = categories[category]
|
|
category_display = category_names.get(category, category.title().replace('_', ' '))
|
|
# Create anchor ID from category (lowercase, replace spaces with hyphens)
|
|
category_id = category.lower().replace('_', '-').replace(' ', '-')
|
|
|
|
commands_html += f'<div class="category-section" id="commands-{category_id}">\n'
|
|
commands_html += f' <h2 class="category-title"><a href="#commands-{category_id}" class="anchor-link">{escape_html(category_display)}</a></h2>\n'
|
|
commands_html += f' <div class="commands-grid">\n'
|
|
|
|
for cmd_name, cmd_instance in category_commands:
|
|
primary_name = cmd_instance.name if hasattr(cmd_instance, 'name') else cmd_name
|
|
keywords = getattr(cmd_instance, 'keywords', [])
|
|
|
|
# Filter out the primary name from keywords to avoid duplication
|
|
aliases = [k for k in keywords if k.lower() != primary_name.lower()]
|
|
|
|
# Get channel restriction info
|
|
channel_info = get_channel_info(cmd_instance, monitor_channels)
|
|
|
|
# Get usage information including usage syntax, examples, parameters, and sub-commands
|
|
try:
|
|
usage_info = cmd_instance.get_usage_info()
|
|
usage_syntax = usage_info.get('usage', '')
|
|
examples = usage_info.get('examples', [])
|
|
parameters = usage_info.get('parameters', [])
|
|
subcommands = usage_info.get('subcommands', [])
|
|
# Use short_description for website if available, otherwise fallback to description
|
|
short_desc = usage_info.get('short_description', '')
|
|
description = short_desc if short_desc else usage_info.get('description', 'No description available')
|
|
except Exception:
|
|
usage_syntax = ''
|
|
examples = []
|
|
parameters = []
|
|
subcommands = []
|
|
description = getattr(cmd_instance, 'description', 'No description available')
|
|
|
|
commands_html += f' <div class="command-card">\n'
|
|
commands_html += f' <div class="command-header">\n'
|
|
commands_html += f' <h3 class="command-name">{escape_html(primary_name)}</h3>\n'
|
|
if aliases:
|
|
commands_html += f' <div class="command-keywords">\n'
|
|
# Show first 5 aliases
|
|
visible_aliases = aliases[:5]
|
|
hidden_aliases = aliases[5:]
|
|
|
|
for alias in visible_aliases:
|
|
commands_html += f' <span class="keyword-badge">{escape_html(alias)}</span>\n'
|
|
|
|
if hidden_aliases:
|
|
# Store hidden aliases in data attribute and create expandable badge
|
|
hidden_aliases_json = escape_html(','.join(hidden_aliases))
|
|
commands_html += f' <span class="keyword-badge keyword-expand" data-hidden="{hidden_aliases_json}" data-command="{escape_html(primary_name)}">+{len(hidden_aliases)} more</span>\n'
|
|
|
|
commands_html += f' </div>\n'
|
|
commands_html += f' </div>\n'
|
|
commands_html += f' <p class="command-description">{escape_html(description)}</p>\n'
|
|
|
|
# Render usage, examples, parameters, subcommands
|
|
try:
|
|
|
|
# Render usage syntax
|
|
if usage_syntax:
|
|
commands_html += f' <div class="command-usage"><code>{escape_html(usage_syntax)}</code></div>\n'
|
|
|
|
# Render parameters
|
|
if parameters:
|
|
commands_html += f' <div class="command-params">\n'
|
|
commands_html += f' <div class="params-header">Parameters:</div>\n'
|
|
for param in parameters:
|
|
param_name = param.get('name', '')
|
|
param_desc = param.get('description', '')
|
|
if param_name and param_desc:
|
|
commands_html += f' <div class="param-item">\n'
|
|
commands_html += f' <span class="param-name">{escape_html(param_name)}</span>\n'
|
|
commands_html += f' <span class="param-desc">{escape_html(param_desc)}</span>\n'
|
|
commands_html += f' </div>\n'
|
|
commands_html += f' </div>\n'
|
|
|
|
# Render subcommands
|
|
if subcommands:
|
|
commands_html += f' <div class="command-subcommands">\n'
|
|
commands_html += f' <div class="subcommands-header">Sub-commands:</div>\n'
|
|
for subcmd in subcommands:
|
|
subcmd_name = subcmd.get('name', '')
|
|
subcmd_desc = subcmd.get('description', '')
|
|
if subcmd_name and subcmd_desc:
|
|
commands_html += f' <div class="subcommand-item">\n'
|
|
commands_html += f' <span class="subcommand-name">{escape_html(subcmd_name)}</span>\n'
|
|
commands_html += f' <span class="subcommand-desc">{escape_html(subcmd_desc)}</span>\n'
|
|
commands_html += f' </div>\n'
|
|
commands_html += f' </div>\n'
|
|
except Exception as e:
|
|
# Silently fail if get_usage_info() is not available or fails
|
|
pass
|
|
|
|
if channel_info:
|
|
commands_html += f' <div class="command-channels">{escape_html(channel_info)}</div>\n'
|
|
commands_html += f' </div>\n'
|
|
|
|
commands_html += f' </div>\n'
|
|
commands_html += f'</div>\n'
|
|
|
|
# Build channels HTML if channels are available
|
|
channels_html = ""
|
|
if channels_data:
|
|
channels_html += '<div class="category-section" id="channels">\n'
|
|
channels_html += ' <h2 class="category-title"><a href="#channels" class="anchor-link">Available Channels</a></h2>\n'
|
|
channels_html += ' <p class="channels-intro">These are semi-public channels that are in use on our local mesh! Join them to connect with others with common interests! <br/> To add these channels to your client, click on the three dot menu and select "Add Channel".</p>\n'
|
|
|
|
# Sort categories: general first, then alphabetically
|
|
sorted_categories = ['general'] + sorted([c for c in channels_data.keys() if c != 'general'])
|
|
|
|
for category in sorted_categories:
|
|
if category not in channels_data or not channels_data[category]:
|
|
continue
|
|
|
|
category_display = category.title().replace('_', ' ') if category != 'general' else 'General Channels'
|
|
category_id = category.lower().replace('_', '-').replace(' ', '-')
|
|
channels_html += f' <div class="channel-category" id="channels-{category_id}">\n'
|
|
channels_html += f' <h3 class="channel-category-title"><a href="#channels-{category_id}" class="anchor-link">{escape_html(category_display)}</a></h3>\n'
|
|
channels_html += f' <div class="channels-grid">\n'
|
|
|
|
# Sort channels alphabetically
|
|
sorted_channels = sorted(channels_data[category].items())
|
|
for channel_name, description in sorted_channels:
|
|
channels_html += f' <div class="channel-card">\n'
|
|
channels_html += f' <div class="channel-name">{escape_html(channel_name)}</div>\n'
|
|
channels_html += f' <div class="channel-description">{escape_html(description)}</div>\n'
|
|
channels_html += f' </div>\n'
|
|
|
|
channels_html += f' </div>\n'
|
|
channels_html += f' </div>\n'
|
|
|
|
channels_html += '</div>\n'
|
|
|
|
html_content = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{escape_html(title)}</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="{STYLES[style]['fonts_url']}" rel="stylesheet">
|
|
<style>
|
|
:root {{
|
|
{STYLES[style]['css_vars']}
|
|
}}
|
|
|
|
* {{
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}}
|
|
|
|
html {{
|
|
scroll-behavior: smooth;
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Outfit', sans-serif;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
line-height: 1.6;
|
|
}}
|
|
|
|
/* Atmospheric background */
|
|
.atmosphere {{
|
|
position: fixed;
|
|
inset: 0;
|
|
background:
|
|
radial-gradient(ellipse 80% 50% at 20% 0%, rgba(0, 212, 255, 0.08) 0%, transparent 50%),
|
|
radial-gradient(ellipse 60% 40% at 80% 100%, rgba(0, 255, 200, 0.05) 0%, transparent 50%),
|
|
radial-gradient(ellipse 100% 100% at 50% 50%, var(--bg-primary) 0%, #060a0f 100%);
|
|
pointer-events: none;
|
|
z-index: -1;
|
|
}}
|
|
|
|
/* Subtle grid overlay */
|
|
.grid-overlay {{
|
|
position: fixed;
|
|
inset: 0;
|
|
background-image:
|
|
linear-gradient(rgba(255,255,255,0.01) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(255,255,255,0.01) 1px, transparent 1px);
|
|
background-size: 60px 60px;
|
|
pointer-events: none;
|
|
z-index: -1;
|
|
}}
|
|
|
|
.container {{
|
|
max-width: 1600px;
|
|
margin: 0 auto;
|
|
padding: 3rem 2rem;
|
|
min-height: 100vh;
|
|
position: relative;
|
|
z-index: 1;
|
|
display: grid;
|
|
grid-template-columns: 280px 1fr;
|
|
gap: 3rem;
|
|
}}
|
|
|
|
.sidebar-nav {{
|
|
position: sticky;
|
|
top: 2rem;
|
|
height: fit-content;
|
|
max-height: calc(100vh - 4rem);
|
|
overflow-y: auto;
|
|
background: var(--bg-card);
|
|
border-radius: 16px;
|
|
border: 1px solid var(--border-subtle);
|
|
padding: 1.5rem;
|
|
z-index: 100;
|
|
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
opacity: 1;
|
|
filter: none;
|
|
}}
|
|
|
|
.mobile-menu-toggle {{
|
|
display: none;
|
|
position: fixed;
|
|
top: 1.5rem;
|
|
right: 1.5rem;
|
|
z-index: 1001;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border-subtle);
|
|
border-radius: 12px;
|
|
padding: 0.75rem;
|
|
cursor: pointer;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 48px;
|
|
height: 48px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
transition: all 0.3s ease;
|
|
}}
|
|
|
|
.mobile-menu-toggle:hover {{
|
|
background: var(--bg-card-hover);
|
|
border-color: var(--accent-cyan);
|
|
}}
|
|
|
|
.mobile-menu-toggle.active {{
|
|
background: var(--bg-card-hover);
|
|
border-color: var(--accent-cyan);
|
|
}}
|
|
|
|
.hamburger {{
|
|
width: 24px;
|
|
height: 2px;
|
|
background: var(--accent-cyan);
|
|
border-radius: 2px;
|
|
transition: all 0.3s ease;
|
|
}}
|
|
|
|
.mobile-menu-toggle.active .hamburger:nth-child(1) {{
|
|
transform: rotate(45deg) translate(7px, 7px);
|
|
}}
|
|
|
|
.mobile-menu-toggle.active .hamburger:nth-child(2) {{
|
|
opacity: 0;
|
|
}}
|
|
|
|
.mobile-menu-toggle.active .hamburger:nth-child(3) {{
|
|
transform: rotate(-45deg) translate(7px, -7px);
|
|
}}
|
|
|
|
.sidebar-overlay {{
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
z-index: 1000;
|
|
backdrop-filter: none;
|
|
-webkit-backdrop-filter: none;
|
|
pointer-events: none;
|
|
}}
|
|
|
|
.sidebar-overlay.visible {{
|
|
pointer-events: auto;
|
|
}}
|
|
|
|
@media (max-width: 1200px) {{
|
|
.sidebar-overlay.visible {{
|
|
/* Don't cover the sidebar area - start overlay after sidebar width */
|
|
left: 280px !important;
|
|
}}
|
|
}}
|
|
|
|
@media (min-width: 1201px) {{
|
|
.sidebar-overlay {{
|
|
backdrop-filter: blur(4px);
|
|
-webkit-backdrop-filter: blur(4px);
|
|
}}
|
|
}}
|
|
|
|
@media (max-width: 1200px) {{
|
|
.sidebar-overlay {{
|
|
backdrop-filter: none !important;
|
|
-webkit-backdrop-filter: none !important;
|
|
z-index: 1000 !important;
|
|
}}
|
|
|
|
.sidebar-overlay.visible {{
|
|
pointer-events: auto !important;
|
|
}}
|
|
|
|
/* Ensure sidebar is always clickable above overlay */
|
|
.sidebar-nav {{
|
|
pointer-events: auto !important;
|
|
}}
|
|
|
|
.sidebar-nav * {{
|
|
pointer-events: auto !important;
|
|
}}
|
|
}}
|
|
|
|
.sidebar-overlay.visible {{
|
|
display: block;
|
|
}}
|
|
|
|
.sidebar-nav::-webkit-scrollbar {{
|
|
width: 6px;
|
|
}}
|
|
|
|
.sidebar-nav::-webkit-scrollbar-track {{
|
|
background: var(--bg-secondary);
|
|
border-radius: 3px;
|
|
}}
|
|
|
|
.sidebar-nav::-webkit-scrollbar-thumb {{
|
|
background: var(--border-subtle);
|
|
border-radius: 3px;
|
|
}}
|
|
|
|
.sidebar-nav::-webkit-scrollbar-thumb:hover {{
|
|
background: rgba(255,255,255,0.15);
|
|
}}
|
|
|
|
.nav-header {{
|
|
margin-bottom: 1.5rem;
|
|
padding-bottom: 1rem;
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
}}
|
|
|
|
.nav-header h3 {{
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
font-size: 0.85rem;
|
|
}}
|
|
|
|
.nav-list {{
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}}
|
|
|
|
.nav-section-header {{
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
color: var(--text-muted);
|
|
font-weight: 600;
|
|
margin-top: 1.5rem;
|
|
margin-bottom: 0.75rem;
|
|
padding-left: 0.5rem;
|
|
}}
|
|
|
|
.nav-section-header:first-child {{
|
|
margin-top: 0;
|
|
}}
|
|
|
|
.nav-sublist {{
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0 0 1rem 0;
|
|
padding-left: 0.5rem;
|
|
}}
|
|
|
|
.nav-link {{
|
|
display: block;
|
|
padding: 0.6rem 0.75rem;
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
border-radius: 8px;
|
|
font-size: 0.9rem;
|
|
transition: all 0.2s ease;
|
|
margin-bottom: 0.25rem;
|
|
cursor: pointer;
|
|
-webkit-tap-highlight-color: rgba(0, 255, 200, 0.2);
|
|
}}
|
|
|
|
.nav-link:hover {{
|
|
background: var(--bg-card-hover);
|
|
color: var(--accent-cyan);
|
|
transform: translateX(4px);
|
|
}}
|
|
|
|
.nav-link:active,
|
|
.nav-link.active {{
|
|
background: rgba(0, 255, 200, 0.1);
|
|
color: var(--accent-cyan);
|
|
border-left: 3px solid var(--accent-cyan);
|
|
padding-left: calc(0.75rem - 3px);
|
|
}}
|
|
|
|
.nav-sublink {{
|
|
padding-left: 1.25rem;
|
|
font-size: 0.85rem;
|
|
color: var(--text-muted);
|
|
}}
|
|
|
|
.nav-sublink:hover {{
|
|
color: var(--accent-blue);
|
|
}}
|
|
|
|
.main-content {{
|
|
min-width: 0;
|
|
}}
|
|
|
|
header {{
|
|
margin-bottom: 4rem;
|
|
padding: 0;
|
|
}}
|
|
|
|
.header-content {{
|
|
background: var(--bg-card);
|
|
border-radius: 20px;
|
|
border: 1px solid var(--border-subtle);
|
|
padding: 3rem 2.5rem;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}}
|
|
|
|
.header-content::before {{
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 3px;
|
|
background: linear-gradient(90deg, var(--accent-blue), var(--accent-cyan));
|
|
}}
|
|
|
|
.header-title {{
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}}
|
|
|
|
.logo-icon {{
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 3.5rem;
|
|
flex-shrink: 0;
|
|
line-height: 1;
|
|
}}
|
|
|
|
h1 {{
|
|
font-size: 3rem;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
color: var(--accent-cyan);
|
|
letter-spacing: -0.02em;
|
|
font-family: 'Outfit', sans-serif;
|
|
}}
|
|
|
|
.intro {{
|
|
font-size: 1.1rem;
|
|
color: var(--text-secondary);
|
|
max-width: 900px;
|
|
line-height: 1.8;
|
|
margin-top: 0.5rem;
|
|
}}
|
|
|
|
.channel-highlight {{
|
|
color: var(--accent-cyan);
|
|
font-weight: 500;
|
|
}}
|
|
|
|
.category-section {{
|
|
margin-bottom: 3rem;
|
|
}}
|
|
|
|
.category-title {{
|
|
font-size: 1.75rem;
|
|
font-weight: 600;
|
|
margin-bottom: 1.5rem;
|
|
color: var(--text-primary);
|
|
padding-bottom: 1rem;
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
letter-spacing: -0.01em;
|
|
}}
|
|
|
|
.category-title .anchor-link,
|
|
.channel-category-title .anchor-link {{
|
|
color: inherit;
|
|
text-decoration: none;
|
|
position: relative;
|
|
display: inline-block;
|
|
}}
|
|
|
|
.category-title .anchor-link:hover,
|
|
.channel-category-title .anchor-link:hover {{
|
|
color: var(--accent-cyan);
|
|
}}
|
|
|
|
.category-title .anchor-link::before {{
|
|
content: '#';
|
|
position: absolute;
|
|
left: -1.5rem;
|
|
opacity: 0;
|
|
color: var(--accent-blue);
|
|
font-weight: 400;
|
|
transition: opacity 0.2s ease;
|
|
}}
|
|
|
|
.category-title:hover .anchor-link::before,
|
|
.channel-category-title:hover .anchor-link::before {{
|
|
opacity: 0.5;
|
|
}}
|
|
|
|
.category-section,
|
|
.channel-category {{
|
|
scroll-margin-top: 2rem;
|
|
}}
|
|
|
|
.commands-grid {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: 1.25rem;
|
|
margin-bottom: 2rem;
|
|
}}
|
|
|
|
.command-card {{
|
|
background: var(--bg-card);
|
|
border-radius: 16px;
|
|
padding: 1.5rem;
|
|
border: 1px solid var(--border-subtle);
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}}
|
|
|
|
.command-card::before {{
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 3px;
|
|
background: linear-gradient(90deg, var(--accent-blue), transparent);
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
}}
|
|
|
|
.command-card:hover {{
|
|
background: var(--bg-card-hover);
|
|
border-color: rgba(255,255,255,0.1);
|
|
transform: translateY(-2px);
|
|
}}
|
|
|
|
.command-card:hover::before {{
|
|
opacity: 1;
|
|
}}
|
|
|
|
.command-header {{
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
margin-bottom: 1rem;
|
|
}}
|
|
|
|
.command-name {{
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
color: var(--accent-blue);
|
|
margin: 0;
|
|
letter-spacing: -0.01em;
|
|
}}
|
|
|
|
.command-keywords {{
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}}
|
|
|
|
.keyword-badge {{
|
|
background: rgba(0, 212, 255, 0.1);
|
|
color: var(--accent-cyan);
|
|
padding: 0.35rem 0.75rem;
|
|
border-radius: 8px;
|
|
font-size: 0.8rem;
|
|
font-weight: 500;
|
|
border: 1px solid rgba(0, 212, 255, 0.2);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}}
|
|
|
|
.keyword-expand {{
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}}
|
|
|
|
.keyword-expand:hover {{
|
|
background: rgba(0, 212, 255, 0.2);
|
|
border-color: var(--accent-cyan);
|
|
transform: scale(1.05);
|
|
}}
|
|
|
|
.keyword-expand.expanded {{
|
|
display: none;
|
|
}}
|
|
|
|
.keyword-hidden {{
|
|
display: none;
|
|
}}
|
|
|
|
.keyword-hidden.visible {{
|
|
display: inline-block;
|
|
}}
|
|
|
|
.command-description {{
|
|
color: var(--text-secondary);
|
|
line-height: 1.7;
|
|
margin-bottom: 0.75rem;
|
|
font-size: 0.95rem;
|
|
}}
|
|
|
|
.command-usage {{
|
|
margin: 0.75rem 0;
|
|
padding: 0.6rem 0.9rem;
|
|
background: var(--bg-secondary);
|
|
border-radius: 8px;
|
|
border-left: 3px solid var(--accent-blue);
|
|
}}
|
|
|
|
.command-usage code {{
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 0.85rem;
|
|
color: var(--accent-cyan);
|
|
}}
|
|
|
|
.command-params {{
|
|
margin-top: 0.75rem;
|
|
padding-top: 0.75rem;
|
|
border-top: 1px solid var(--border-subtle);
|
|
}}
|
|
|
|
.params-header {{
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
margin-bottom: 0.5rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}}
|
|
|
|
.param-item {{
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.4rem;
|
|
font-size: 0.85rem;
|
|
align-items: flex-start;
|
|
}}
|
|
|
|
.param-name {{
|
|
font-family: 'JetBrains Mono', monospace;
|
|
color: var(--accent-orange);
|
|
font-weight: 500;
|
|
min-width: 80px;
|
|
flex-shrink: 0;
|
|
}}
|
|
|
|
.param-desc {{
|
|
color: var(--text-secondary);
|
|
flex: 1;
|
|
line-height: 1.4;
|
|
}}
|
|
|
|
.command-subcommands {{
|
|
margin-top: 1rem;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid var(--border-subtle);
|
|
}}
|
|
|
|
.subcommands-header {{
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
margin-bottom: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}}
|
|
|
|
.subcommand-item {{
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
margin-bottom: 0.5rem;
|
|
font-size: 0.9rem;
|
|
align-items: flex-start;
|
|
}}
|
|
|
|
.subcommand-name {{
|
|
font-family: 'JetBrains Mono', monospace;
|
|
color: var(--accent-cyan);
|
|
font-weight: 500;
|
|
min-width: 100px;
|
|
flex-shrink: 0;
|
|
}}
|
|
|
|
.subcommand-desc {{
|
|
color: var(--text-secondary);
|
|
flex: 1;
|
|
line-height: 1.5;
|
|
}}
|
|
|
|
.command-channels {{
|
|
color: var(--accent-cyan);
|
|
font-size: 0.85rem;
|
|
margin-top: 0.75rem;
|
|
padding: 0.6rem 0.9rem;
|
|
background: rgba(0, 255, 200, 0.08);
|
|
border-radius: 8px;
|
|
border-left: 3px solid var(--accent-cyan);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-weight: 500;
|
|
}}
|
|
|
|
.channels-intro {{
|
|
color: var(--text-secondary);
|
|
font-size: 1rem;
|
|
margin-bottom: 2rem;
|
|
line-height: 1.7;
|
|
}}
|
|
|
|
.channel-category {{
|
|
margin-bottom: 2.5rem;
|
|
}}
|
|
|
|
.channel-category-title {{
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
color: var(--accent-blue);
|
|
margin-bottom: 1rem;
|
|
letter-spacing: -0.01em;
|
|
}}
|
|
|
|
.channel-category-title .anchor-link::before {{
|
|
content: '#';
|
|
position: absolute;
|
|
left: -1.2rem;
|
|
opacity: 0;
|
|
color: var(--accent-blue);
|
|
font-weight: 400;
|
|
transition: opacity 0.2s ease;
|
|
}}
|
|
|
|
.channels-grid {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 1rem;
|
|
}}
|
|
|
|
.channel-card {{
|
|
background: var(--bg-card);
|
|
border-radius: 12px;
|
|
padding: 1.25rem;
|
|
border: 1px solid var(--border-subtle);
|
|
transition: all 0.3s ease;
|
|
}}
|
|
|
|
.channel-card:hover {{
|
|
background: var(--bg-card-hover);
|
|
border-color: rgba(0, 212, 255, 0.3);
|
|
transform: translateY(-2px);
|
|
}}
|
|
|
|
.channel-name {{
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
color: var(--accent-cyan);
|
|
margin-bottom: 0.5rem;
|
|
}}
|
|
|
|
.channel-description {{
|
|
color: var(--text-secondary);
|
|
font-size: 0.9rem;
|
|
line-height: 1.6;
|
|
}}
|
|
|
|
footer {{
|
|
text-align: center;
|
|
margin-top: 4rem;
|
|
padding: 2rem;
|
|
color: var(--text-muted);
|
|
font-size: 0.9rem;
|
|
border-top: 1px solid var(--border-subtle);
|
|
}}
|
|
|
|
@media (max-width: 1200px) {{
|
|
.commands-grid {{
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
}}
|
|
}}
|
|
|
|
@media (max-width: 1200px) {{
|
|
/* Disable all backdrop filters on mobile */
|
|
* {{
|
|
backdrop-filter: none !important;
|
|
-webkit-backdrop-filter: none !important;
|
|
}}
|
|
|
|
.container {{
|
|
grid-template-columns: 1fr !important;
|
|
}}
|
|
|
|
.mobile-menu-toggle {{
|
|
display: flex;
|
|
}}
|
|
|
|
.sidebar-nav {{
|
|
position: fixed !important;
|
|
top: 0 !important;
|
|
left: -320px !important;
|
|
width: 280px !important;
|
|
height: 100vh !important;
|
|
max-height: 100vh !important;
|
|
margin: 0 !important;
|
|
padding-top: 4rem !important;
|
|
border-radius: 0 !important;
|
|
border-left: none !important;
|
|
border-top: none !important;
|
|
border-bottom: none !important;
|
|
border-right: 1px solid var(--border-subtle) !important;
|
|
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
|
z-index: 1002 !important;
|
|
opacity: 1 !important;
|
|
filter: none !important;
|
|
backdrop-filter: none !important;
|
|
-webkit-backdrop-filter: none !important;
|
|
transform: translateZ(0) !important;
|
|
will-change: left !important;
|
|
isolation: isolate !important;
|
|
background: var(--bg-card) !important;
|
|
visibility: visible !important;
|
|
-webkit-font-smoothing: antialiased !important;
|
|
-moz-osx-font-smoothing: grayscale !important;
|
|
overflow-y: auto !important;
|
|
overflow-x: hidden !important;
|
|
-webkit-overflow-scrolling: touch !important;
|
|
pointer-events: auto !important;
|
|
touch-action: pan-y !important;
|
|
-webkit-transform: translateZ(0) !important;
|
|
transform: translateZ(0) !important;
|
|
}}
|
|
|
|
.sidebar-nav.open {{
|
|
left: 0 !important;
|
|
}}
|
|
|
|
.sidebar-nav * {{
|
|
opacity: 1 !important;
|
|
filter: none !important;
|
|
backdrop-filter: none !important;
|
|
-webkit-backdrop-filter: none !important;
|
|
visibility: visible !important;
|
|
-webkit-font-smoothing: antialiased !important;
|
|
-moz-osx-font-smoothing: grayscale !important;
|
|
pointer-events: auto !important;
|
|
}}
|
|
|
|
.sidebar-nav .nav-header h3 {{
|
|
color: #e8edf4 !important;
|
|
opacity: 1 !important;
|
|
text-shadow: none !important;
|
|
}}
|
|
|
|
.sidebar-nav .nav-link {{
|
|
color: #8892a4 !important;
|
|
opacity: 1 !important;
|
|
text-shadow: none !important;
|
|
pointer-events: auto !important;
|
|
cursor: pointer !important;
|
|
-webkit-tap-highlight-color: rgba(0, 255, 200, 0.2) !important;
|
|
}}
|
|
|
|
.sidebar-nav .nav-link:hover,
|
|
.sidebar-nav .nav-link:active {{
|
|
color: #00ffc8 !important;
|
|
opacity: 1 !important;
|
|
}}
|
|
|
|
.sidebar-nav .nav-section-header {{
|
|
color: #8892a4 !important;
|
|
opacity: 1 !important;
|
|
text-shadow: none !important;
|
|
}}
|
|
|
|
.main-content {{
|
|
margin-top: 0;
|
|
}}
|
|
}}
|
|
|
|
@media (min-width: 1201px) {{
|
|
.sidebar-nav {{
|
|
position: sticky !important;
|
|
top: 2rem !important;
|
|
left: auto !important;
|
|
width: auto !important;
|
|
height: fit-content !important;
|
|
max-height: calc(100vh - 4rem) !important;
|
|
margin: 0 !important;
|
|
padding: 1.5rem !important;
|
|
border-radius: 16px !important;
|
|
border: 1px solid var(--border-subtle) !important;
|
|
z-index: 100 !important;
|
|
opacity: 1 !important;
|
|
filter: none !important;
|
|
backdrop-filter: none !important;
|
|
-webkit-backdrop-filter: none !important;
|
|
transform: none !important;
|
|
will-change: auto !important;
|
|
isolation: auto !important;
|
|
background: var(--bg-card) !important;
|
|
visibility: visible !important;
|
|
}}
|
|
}}
|
|
|
|
@media (max-width: 768px) {{
|
|
.container {{
|
|
padding: 2rem 1rem;
|
|
}}
|
|
|
|
.header-content {{
|
|
padding: 2rem 1.5rem;
|
|
}}
|
|
|
|
.header-title {{
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 0.75rem;
|
|
}}
|
|
|
|
.logo-icon {{
|
|
width: 48px;
|
|
height: 48px;
|
|
font-size: 1.5rem;
|
|
}}
|
|
|
|
h1 {{
|
|
font-size: 2.25rem;
|
|
}}
|
|
|
|
.commands-grid {{
|
|
grid-template-columns: 1fr;
|
|
}}
|
|
|
|
.sidebar-nav {{
|
|
padding: 1rem;
|
|
width: calc(100vw - 3rem);
|
|
left: calc(-100vw + 3rem);
|
|
}}
|
|
|
|
.sidebar-nav.open {{
|
|
left: 0;
|
|
}}
|
|
|
|
.mobile-menu-toggle {{
|
|
top: 1rem;
|
|
right: 1rem;
|
|
}}
|
|
}}
|
|
|
|
/* Style-specific CSS overrides */
|
|
{STYLES[style].get('css_overrides', '')}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="atmosphere"></div>
|
|
<div class="grid-overlay"></div>
|
|
|
|
<div class="sidebar-overlay"></div>
|
|
|
|
<button class="mobile-menu-toggle" aria-label="Toggle navigation menu">
|
|
<span class="hamburger"></span>
|
|
<span class="hamburger"></span>
|
|
<span class="hamburger"></span>
|
|
</button>
|
|
|
|
<div class="container">
|
|
{nav_html}
|
|
|
|
<div class="main-content">
|
|
<header>
|
|
<div class="header-content">
|
|
<div class="header-title">
|
|
<h1>{escape_html(bot_name)}</h1>
|
|
</div>
|
|
<div class="intro">
|
|
{escape_html(introduction).replace(chr(10), '<br>')}{channels_suffix}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main>
|
|
{commands_html}
|
|
{channels_html}
|
|
</main>
|
|
|
|
<footer>
|
|
<p>Generated command reference for {escape_html(bot_name)}</p>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Handle mobile menu toggle
|
|
(function() {{
|
|
'use strict';
|
|
|
|
const menuToggle = document.querySelector('.mobile-menu-toggle');
|
|
const sidebar = document.querySelector('.sidebar-nav');
|
|
const overlay = document.querySelector('.sidebar-overlay');
|
|
|
|
if (menuToggle && sidebar) {{
|
|
function toggleMenu() {{
|
|
const isOpen = sidebar.classList.contains('open');
|
|
if (isOpen) {{
|
|
menuToggle.classList.remove('active');
|
|
sidebar.classList.remove('open');
|
|
if (overlay) {{
|
|
overlay.classList.remove('visible');
|
|
}}
|
|
}} else {{
|
|
menuToggle.classList.add('active');
|
|
sidebar.classList.add('open');
|
|
if (overlay) {{
|
|
overlay.classList.add('visible');
|
|
}}
|
|
}}
|
|
}}
|
|
|
|
function closeMenu() {{
|
|
menuToggle.classList.remove('active');
|
|
sidebar.classList.remove('open');
|
|
if (overlay) {{
|
|
overlay.classList.remove('visible');
|
|
}}
|
|
}}
|
|
|
|
menuToggle.addEventListener('click', function(e) {{
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
toggleMenu();
|
|
return false;
|
|
}});
|
|
|
|
// Handle overlay clicks - overlay now only covers area after sidebar
|
|
if (overlay) {{
|
|
overlay.addEventListener('click', function(e) {{
|
|
closeMenu();
|
|
}});
|
|
}}
|
|
|
|
// Handle nav link clicks - don't prevent default, let browser handle navigation
|
|
const navLinks = sidebar.querySelectorAll('.nav-link');
|
|
navLinks.forEach(link => {{
|
|
link.addEventListener('click', function(e) {{
|
|
if (window.innerWidth <= 1200) {{
|
|
// Close menu after a delay to allow navigation
|
|
setTimeout(function() {{
|
|
closeMenu();
|
|
}}, 200);
|
|
}}
|
|
}});
|
|
}});
|
|
}}
|
|
|
|
// Handle keyword expansion
|
|
const expandButtons = document.querySelectorAll('.keyword-expand');
|
|
|
|
expandButtons.forEach(button => {{
|
|
button.addEventListener('click', function() {{
|
|
const hiddenAliases = this.getAttribute('data-hidden').split(',');
|
|
const commandKeywords = this.closest('.command-keywords');
|
|
|
|
// Hide the expand button
|
|
this.classList.add('expanded');
|
|
|
|
// Add hidden aliases as visible badges
|
|
hiddenAliases.forEach(alias => {{
|
|
const badge = document.createElement('span');
|
|
badge.className = 'keyword-badge keyword-hidden visible';
|
|
badge.textContent = alias.trim();
|
|
commandKeywords.appendChild(badge);
|
|
}});
|
|
}});
|
|
}});
|
|
}})();
|
|
</script>
|
|
</body>
|
|
</html>"""
|
|
|
|
return html_content
|
|
|
|
|
|
def list_styles():
|
|
"""Print available styles with descriptions."""
|
|
print("Available styles:\n")
|
|
|
|
# Calculate max name length for alignment
|
|
max_len = max(len(name) for name in STYLES.keys())
|
|
|
|
for style_name, style_info in STYLES.items():
|
|
padding = ' ' * (max_len - len(style_name))
|
|
print(f" {style_name}{padding} {style_info['name']}")
|
|
print(f" {' ' * max_len} {style_info['description']}\n")
|
|
|
|
|
|
def generate_samples(config_file):
|
|
"""Generate sample HTML files for all styles with an index page."""
|
|
logger = logging.getLogger(__name__)
|
|
|
|
logger.info(f"Reading config from {config_file}")
|
|
config = read_config(config_file)
|
|
|
|
# Get bot root directory
|
|
bot_root = os.path.dirname(os.path.abspath(config_file))
|
|
if not bot_root:
|
|
bot_root = os.getcwd()
|
|
|
|
# Get bot information
|
|
bot_name = get_bot_name(config)
|
|
admin_commands = get_admin_commands(config)
|
|
title = get_website_title(config)
|
|
introduction = get_website_intro(config)
|
|
|
|
# Get monitor channels (quoted or unquoted)
|
|
monitor_channels_str = strip_optional_quotes(config.get('Channels', 'monitor_channels', fallback=''))
|
|
monitor_channels = [ch.strip() for ch in monitor_channels_str.split(',') if ch.strip()]
|
|
|
|
# Load channels from config
|
|
channels_data = load_channels_from_config(config)
|
|
|
|
# Setup minimal bot for plugin loading
|
|
minimal_bot = MinimalBot(config, logger)
|
|
|
|
# Initialize database manager if database exists
|
|
db_path = get_database_path(config, bot_root)
|
|
if db_path and os.path.exists(db_path):
|
|
try:
|
|
minimal_bot.db_manager = DBManager(minimal_bot, db_path)
|
|
except Exception as e:
|
|
logger.warning(f"Could not load database: {e}")
|
|
minimal_bot.db_manager = None
|
|
else:
|
|
minimal_bot.db_manager = None
|
|
|
|
# Load plugins
|
|
plugin_loader = PluginLoader(minimal_bot)
|
|
commands = plugin_loader.load_all_plugins()
|
|
|
|
# Filter out admin and hidden commands
|
|
public_commands = {
|
|
name: cmd for name, cmd in commands.items()
|
|
if name not in admin_commands and not getattr(cmd, 'hidden', False)
|
|
}
|
|
|
|
# Sort commands
|
|
sorted_commands = sorted(public_commands.items(), key=lambda x: x[0])
|
|
|
|
# Create website directory
|
|
output_dir = os.path.join(bot_root, 'website')
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# Generate a page for each style
|
|
generated_files = []
|
|
for style_name in STYLES.keys():
|
|
logger.info(f"Generating {style_name}.html...")
|
|
|
|
html_content = generate_html(
|
|
bot_name=bot_name,
|
|
title=title,
|
|
introduction=introduction,
|
|
commands=sorted_commands,
|
|
monitor_channels=monitor_channels,
|
|
channels_data=channels_data,
|
|
style=style_name
|
|
)
|
|
|
|
output_path = os.path.join(output_dir, f'{style_name}.html')
|
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|
f.write(html_content)
|
|
|
|
generated_files.append(style_name)
|
|
logger.info(f" ✓ {output_path}")
|
|
|
|
# Generate index.html with links to all styles
|
|
logger.info("Generating index.html...")
|
|
|
|
index_html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{escape_html(bot_name)} - Style Samples</title>
|
|
<style>
|
|
* {{
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}}
|
|
|
|
body {{
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
line-height: 1.6;
|
|
padding: 3rem 2rem;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
background: #f5f5f5;
|
|
}}
|
|
|
|
h1 {{
|
|
font-size: 2.5rem;
|
|
margin-bottom: 0.5rem;
|
|
color: #1a1a1a;
|
|
}}
|
|
|
|
p {{
|
|
color: #666;
|
|
margin-bottom: 2rem;
|
|
}}
|
|
|
|
.styles-grid {{
|
|
display: grid;
|
|
gap: 1rem;
|
|
}}
|
|
|
|
.style-link {{
|
|
display: block;
|
|
padding: 1.5rem;
|
|
background: white;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
transition: all 0.2s ease;
|
|
}}
|
|
|
|
.style-link:hover {{
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
border-color: #0052cc;
|
|
}}
|
|
|
|
.style-name {{
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
color: #0052cc;
|
|
margin-bottom: 0.25rem;
|
|
}}
|
|
|
|
.style-description {{
|
|
color: #666;
|
|
font-size: 0.875rem;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>{escape_html(bot_name)} Style Samples</h1>
|
|
<p>Browse different visual styles for the command documentation.</p>
|
|
|
|
<div class="styles-grid">
|
|
"""
|
|
|
|
for style_name in STYLES.keys():
|
|
style_info = STYLES[style_name]
|
|
index_html += f""" <a href="{style_name}.html" class="style-link">
|
|
<div class="style-name">{escape_html(style_info['name'])}</div>
|
|
<div class="style-description">{escape_html(style_info['description'])}</div>
|
|
</a>
|
|
"""
|
|
|
|
index_html += """ </div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
index_path = os.path.join(output_dir, 'index.html')
|
|
with open(index_path, 'w', encoding='utf-8') as f:
|
|
f.write(index_html)
|
|
|
|
logger.info(f" ✓ {index_path}")
|
|
logger.info(f"\nGenerated {len(generated_files)} style samples + index.html in {output_dir}")
|
|
print(f"\n✓ Sample files generated in {output_dir}/")
|
|
print(f" - index.html (style browser)")
|
|
for style_name in generated_files:
|
|
print(f" - {style_name}.html")
|
|
|
|
|
|
def main():
|
|
"""Main function"""
|
|
logger = setup_logging()
|
|
|
|
# Parse command-line arguments
|
|
parser = argparse.ArgumentParser(
|
|
description='Generate a static website for the bot commands and channels.',
|
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
)
|
|
|
|
parser.add_argument(
|
|
'config',
|
|
nargs='?',
|
|
default='config.ini',
|
|
help='Path to config.ini file (default: config.ini)'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--style',
|
|
choices=list(STYLES.keys()),
|
|
default='default',
|
|
help='CSS style theme for the website (default: default)'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--list-styles',
|
|
action='store_true',
|
|
help='List all available styles and exit'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--sample',
|
|
action='store_true',
|
|
help='Generate sample pages for all styles with an index.html'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Handle --list-styles flag
|
|
if args.list_styles:
|
|
list_styles()
|
|
sys.exit(0)
|
|
|
|
# Handle --sample flag
|
|
if args.sample:
|
|
generate_samples(args.config)
|
|
sys.exit(0)
|
|
|
|
config_file = args.config
|
|
style = args.style
|
|
|
|
try:
|
|
# Read config
|
|
logger.info(f"Reading config from {config_file}")
|
|
config = read_config(config_file)
|
|
|
|
# Get bot root directory
|
|
bot_root = os.path.dirname(os.path.abspath(config_file))
|
|
if not bot_root:
|
|
bot_root = os.getcwd()
|
|
|
|
# Get bot information
|
|
bot_name = get_bot_name(config)
|
|
admin_commands = get_admin_commands(config)
|
|
introduction = get_website_intro(config)
|
|
title = get_website_title(config)
|
|
|
|
# Get monitor channels for channel restriction display (quoted or unquoted)
|
|
monitor_channels_str = strip_optional_quotes(config.get('Channels', 'monitor_channels', fallback=''))
|
|
monitor_channels = [ch.strip() for ch in monitor_channels_str.split(',') if ch.strip()]
|
|
|
|
# Load channels from Channels_List section
|
|
channels_data = load_channels_from_config(config)
|
|
logger.info(f"Loaded {sum(len(chans) for chans in channels_data.values())} channels from {len(channels_data)} categories")
|
|
|
|
logger.info(f"Bot name: {bot_name}")
|
|
logger.info(f"Admin commands: {admin_commands}")
|
|
logger.info(f"Monitor channels: {monitor_channels}")
|
|
|
|
# Setup minimal bot for plugin loading
|
|
minimal_bot = MinimalBot(config, logger)
|
|
|
|
# Initialize database manager if database exists
|
|
db_path = get_database_path(config, bot_root)
|
|
if db_path and os.path.exists(db_path):
|
|
try:
|
|
minimal_bot.db_manager = DBManager(minimal_bot, db_path)
|
|
logger.info(f"Database loaded: {db_path}")
|
|
except Exception as e:
|
|
logger.warning(f"Could not load database: {e}")
|
|
minimal_bot.db_manager = None
|
|
else:
|
|
logger.info("No database found, using default command ordering")
|
|
minimal_bot.db_manager = None
|
|
|
|
# Load plugins
|
|
logger.info("Loading command plugins...")
|
|
plugin_loader = PluginLoader(minimal_bot)
|
|
commands = plugin_loader.load_all_plugins()
|
|
logger.info(f"Loaded {len(commands)} commands")
|
|
|
|
# Filter out admin and hidden commands
|
|
filtered_commands = filter_commands(commands, admin_commands)
|
|
logger.info(f"Filtered to {len(filtered_commands)} public commands")
|
|
|
|
# Log which commands are included
|
|
included_names = sorted([cmd.name if hasattr(cmd, 'name') else name for name, cmd in filtered_commands.items()])
|
|
logger.info(f"Included commands: {', '.join(included_names)}")
|
|
|
|
# Log which commands were excluded (for debugging)
|
|
excluded = []
|
|
for cmd_name, cmd_instance in commands.items():
|
|
if cmd_name not in filtered_commands:
|
|
primary_name = cmd_instance.name if hasattr(cmd_instance, 'name') else cmd_name
|
|
category = getattr(cmd_instance, 'category', 'unknown')
|
|
excluded.append(f"{primary_name} ({category})")
|
|
if excluded:
|
|
logger.debug(f"Excluded commands: {', '.join(excluded)}")
|
|
|
|
# Get command popularity
|
|
popularity = get_command_popularity(db_path, filtered_commands)
|
|
|
|
# Sort commands by popularity
|
|
sorted_commands = sort_commands_by_popularity(filtered_commands, popularity)
|
|
|
|
# Generate HTML
|
|
logger.info("Generating HTML...")
|
|
html_content = generate_html(bot_name, title, introduction, sorted_commands, monitor_channels, channels_data, style)
|
|
|
|
# Create website directory
|
|
website_dir = os.path.join(bot_root, "website")
|
|
os.makedirs(website_dir, exist_ok=True)
|
|
|
|
# Write HTML file
|
|
output_file = os.path.join(website_dir, "index.html")
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
f.write(html_content)
|
|
|
|
logger.info(f"Website generated successfully: {output_file}")
|
|
print(f"\n✓ Website generated: {output_file}")
|
|
print(f" Bot: {bot_name}")
|
|
print(f" Commands: {len(sorted_commands)}")
|
|
print(f" Output: {output_file}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error generating website: {e}", exc_info=True)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|