mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-10-31 01:52:38 +00:00
The 489-line method ran ten independent sections in sequence (contacts, incoming packets, adverts, repeaters, caches, messages, commands, paths, network health, geography), each reading only the cursor, the table list, the stats dict and its window arguments. Each section is now a `_stats_*` method called in the same order inside the same connection and try; their statements are AST-identical to the originals, SQL strings included.
749 lines
32 KiB
Python
749 lines
32 KiB
Python
"""Database statistics, table info, cache view and optimization for the web viewer."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from typing import Any
|
|
|
|
from modules.security_utils import validate_sql_identifier
|
|
from modules.web_viewer.dashboard_stats import humanize_span
|
|
|
|
|
|
class DatabaseAdminMixin:
|
|
"""Mixed into BotDataViewer."""
|
|
|
|
ALLOWED_TABLES: Any
|
|
_bucket_hop_chunks: Any
|
|
_clients_lock: Any
|
|
_contact_has_multibyte_path_evidence: Any
|
|
_db_connection: Any
|
|
_get_cached_contact_multibyte_hop_chunks: Any
|
|
connected_clients: Any
|
|
db_path: Any
|
|
logger: Any
|
|
|
|
def _get_database_stats(self, top_users_window='all', top_commands_window='all',
|
|
top_paths_window='all', top_channels_window='all'):
|
|
"""Get comprehensive database statistics for dashboard"""
|
|
try:
|
|
with self._db_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Get all available tables
|
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
tables = [row[0] for row in cursor.fetchall()]
|
|
|
|
# Filter tables by ALLOWED_TABLES whitelist for security
|
|
tables = [t for t in tables if t in self.ALLOWED_TABLES]
|
|
|
|
with self._clients_lock:
|
|
client_count = len(self.connected_clients)
|
|
|
|
stats = {
|
|
'timestamp': time.time(),
|
|
'connected_clients': client_count,
|
|
'tables': tables
|
|
}
|
|
self._stats_contacts(cursor, tables, stats)
|
|
self._stats_incoming_packets(cursor, tables, stats)
|
|
self._stats_adverts(cursor, tables, stats)
|
|
self._stats_repeaters(cursor, tables, stats)
|
|
self._stats_caches(cursor, tables, stats)
|
|
self._stats_messages(cursor, tables, stats, top_users_window, top_channels_window)
|
|
self._stats_commands(cursor, tables, stats, top_commands_window)
|
|
self._stats_paths(cursor, tables, stats, top_paths_window)
|
|
self._stats_network_health(cursor, tables, stats)
|
|
self._stats_geography(cursor, tables, stats)
|
|
|
|
return stats
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error getting database stats: {e}")
|
|
return {'error': str(e)}
|
|
|
|
def _stats_contacts(self, cursor, tables, stats):
|
|
"""Contact and tracking statistics."""
|
|
if 'complete_contact_tracking' in tables:
|
|
cursor.execute("SELECT COUNT(*) FROM complete_contact_tracking")
|
|
stats['total_contacts'] = cursor.fetchone()[0]
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(*) FROM complete_contact_tracking
|
|
WHERE last_heard > datetime('now', 'localtime', '-24 hours')
|
|
""")
|
|
stats['contacts_24h'] = cursor.fetchone()[0]
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(*) FROM complete_contact_tracking
|
|
WHERE last_heard > datetime('now', 'localtime', '-7 days')
|
|
""")
|
|
stats['contacts_7d'] = cursor.fetchone()[0]
|
|
|
|
# Contacts heard in 7d with multibyte path evidence. Scope observed_paths to 7d so
|
|
# the pie chart matches "last 7 days" (lifetime paths + stale out_bytes_per_hop
|
|
# otherwise inflated the percentage).
|
|
stats['contacts_7d_multibyte_path'] = 0
|
|
chunk_buckets = self._bucket_hop_chunks(set())
|
|
mb_advert_pks: set[str] = set()
|
|
if 'observed_paths' in tables:
|
|
try:
|
|
chunk_buckets = self._bucket_hop_chunks(
|
|
self._get_cached_contact_multibyte_hop_chunks(
|
|
cursor, recent_days=7
|
|
)
|
|
)
|
|
# Use date() — julianday(iso8601) often returns NULL for Python isoformat() strings
|
|
cursor.execute(
|
|
"""
|
|
SELECT DISTINCT public_key FROM observed_paths
|
|
WHERE packet_type = 'advert' AND public_key IS NOT NULL
|
|
AND bytes_per_hop IN (2, 3)
|
|
AND date(last_seen) >= date('now', 'localtime', '-7 days')
|
|
"""
|
|
)
|
|
mb_advert_pks = {
|
|
row["public_key"] for row in cursor.fetchall() if row["public_key"]
|
|
}
|
|
except Exception as e:
|
|
self.logger.debug(f"Could not load multibyte path sets for 7d stats: {e}")
|
|
try:
|
|
cursor.execute(
|
|
"""
|
|
SELECT public_key, role, out_bytes_per_hop
|
|
FROM complete_contact_tracking
|
|
WHERE last_heard > datetime('now', 'localtime', '-7 days')
|
|
"""
|
|
)
|
|
mb_7d = 0
|
|
for row in cursor.fetchall():
|
|
if self._contact_has_multibyte_path_evidence(
|
|
row["public_key"],
|
|
row["role"],
|
|
row["out_bytes_per_hop"],
|
|
mb_advert_pks,
|
|
chunk_buckets,
|
|
):
|
|
mb_7d += 1
|
|
stats['contacts_7d_multibyte_path'] = mb_7d
|
|
except Exception as e:
|
|
self.logger.debug(f"Could not compute contacts_7d_multibyte_path: {e}")
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(*) FROM complete_contact_tracking
|
|
WHERE is_currently_tracked = 1
|
|
""")
|
|
stats['tracked_contacts'] = cursor.fetchone()[0]
|
|
|
|
cursor.execute("""
|
|
SELECT AVG(hop_count) FROM complete_contact_tracking
|
|
WHERE hop_count IS NOT NULL
|
|
""")
|
|
avg_hops = cursor.fetchone()[0]
|
|
stats['avg_hop_count'] = round(avg_hops, 1) if avg_hops else 0
|
|
|
|
cursor.execute("""
|
|
SELECT MAX(hop_count) FROM complete_contact_tracking
|
|
WHERE hop_count IS NOT NULL
|
|
""")
|
|
stats['max_hop_count'] = cursor.fetchone()[0] or 0
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(DISTINCT role) FROM complete_contact_tracking
|
|
WHERE role IS NOT NULL
|
|
""")
|
|
stats['unique_roles'] = cursor.fetchone()[0]
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(DISTINCT device_type) FROM complete_contact_tracking
|
|
WHERE device_type IS NOT NULL
|
|
""")
|
|
stats['unique_device_types'] = cursor.fetchone()[0]
|
|
|
|
def _stats_incoming_packets(self, cursor, tables, stats):
|
|
"""Multi-byte path share of incoming packets over what packet_stream retains."""
|
|
# Incoming packets: multibyte path share over whatever packet_stream
|
|
# actually retains. The key names still say 7d for compatibility,
|
|
# but the window is reported honestly alongside them —
|
|
# packet_stream is pruned at 3 days, so the old label was never true.
|
|
stats['incoming_packets_7d'] = 0
|
|
stats['incoming_packets_7d_multibyte_path'] = 0
|
|
if 'packet_stream' in tables:
|
|
try:
|
|
cutoff_ts = time.time() - 7 * 86400
|
|
cursor.execute(
|
|
"""
|
|
SELECT COUNT(*),
|
|
SUM(CASE WHEN bytes_per_hop IN (2, 3) THEN 1 ELSE 0 END),
|
|
MIN(timestamp)
|
|
FROM packet_stream
|
|
WHERE type = ? AND timestamp > ? AND route_type_name IS NOT NULL
|
|
""",
|
|
("packet", cutoff_ts),
|
|
)
|
|
row = cursor.fetchone()
|
|
stats['incoming_packets_7d'] = row[0] or 0
|
|
stats['incoming_packets_7d_multibyte_path'] = row[1] or 0
|
|
stats['incoming_packets_from'] = row[2]
|
|
stats['incoming_packets_window_label'] = humanize_span(
|
|
time.time() - row[2] if row[2] else None
|
|
)
|
|
except Exception as e:
|
|
self.logger.debug(f"Could not compute incoming packet multibyte stats: {e}")
|
|
|
|
def _stats_adverts(self, cursor, tables, stats):
|
|
"""Advertisement statistics from the daily tracking table."""
|
|
if 'daily_stats' in tables:
|
|
# Total advertisements (all time)
|
|
cursor.execute("""
|
|
SELECT SUM(advert_count) FROM daily_stats
|
|
""")
|
|
total_adverts = cursor.fetchone()[0]
|
|
stats['total_advertisements'] = total_adverts or 0
|
|
|
|
# 24h advertisements
|
|
cursor.execute("""
|
|
SELECT SUM(advert_count) FROM daily_stats
|
|
WHERE date = date('now', 'localtime')
|
|
""")
|
|
stats['advertisements_24h'] = cursor.fetchone()[0] or 0
|
|
|
|
# 7d advertisements (last 7 days, excluding today)
|
|
cursor.execute("""
|
|
SELECT SUM(advert_count) FROM daily_stats
|
|
WHERE date >= date('now', 'localtime', '-7 days') AND date < date('now', 'localtime')
|
|
""")
|
|
stats['advertisements_7d'] = cursor.fetchone()[0] or 0
|
|
|
|
# Nodes per day statistics
|
|
cursor.execute("""
|
|
SELECT COUNT(DISTINCT public_key) FROM daily_stats
|
|
WHERE date = date('now', 'localtime')
|
|
""")
|
|
stats['nodes_24h'] = cursor.fetchone()[0] or 0
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(DISTINCT public_key) FROM daily_stats
|
|
WHERE date >= date('now', 'localtime', '-6 days')
|
|
""")
|
|
stats['nodes_7d'] = cursor.fetchone()[0] or 0
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(DISTINCT public_key) FROM daily_stats
|
|
""")
|
|
stats['nodes_all'] = cursor.fetchone()[0] or 0
|
|
else:
|
|
# Fallback to old method if daily table doesn't exist yet
|
|
if 'complete_contact_tracking' in tables:
|
|
cursor.execute("""
|
|
SELECT SUM(advert_count) FROM complete_contact_tracking
|
|
""")
|
|
total_adverts = cursor.fetchone()[0]
|
|
stats['total_advertisements'] = total_adverts or 0
|
|
|
|
cursor.execute("""
|
|
SELECT SUM(advert_count) FROM complete_contact_tracking
|
|
WHERE last_heard > datetime('now', 'localtime', '-24 hours')
|
|
""")
|
|
stats['advertisements_24h'] = cursor.fetchone()[0] or 0
|
|
|
|
cursor.execute("""
|
|
SELECT SUM(advert_count) FROM complete_contact_tracking
|
|
WHERE last_heard > datetime('now', 'localtime', '-7 days')
|
|
""")
|
|
stats['advertisements_7d'] = cursor.fetchone()[0] or 0
|
|
|
|
def _stats_repeaters(self, cursor, tables, stats):
|
|
"""Repeater contact counts."""
|
|
if 'repeater_contacts' in tables:
|
|
cursor.execute("SELECT COUNT(*) FROM repeater_contacts")
|
|
stats['repeater_contacts'] = cursor.fetchone()[0]
|
|
|
|
cursor.execute("SELECT COUNT(*) FROM repeater_contacts WHERE is_active = 1")
|
|
stats['active_repeater_contacts'] = cursor.fetchone()[0]
|
|
|
|
def _stats_caches(self, cursor, tables, stats):
|
|
"""Cache table entry counts."""
|
|
cache_tables = [t for t in tables if 'cache' in t]
|
|
stats['cache_tables'] = cache_tables
|
|
stats['total_cache_entries'] = 0
|
|
stats['active_cache_entries'] = 0
|
|
|
|
for table in cache_tables:
|
|
try:
|
|
validate_sql_identifier(table)
|
|
except ValueError:
|
|
self.logger.warning(f"Rejecting invalid table name: {table!r}")
|
|
raise
|
|
cursor.execute(f"SELECT COUNT(*) FROM {table}")
|
|
count = cursor.fetchone()[0]
|
|
stats['total_cache_entries'] += count
|
|
stats[f'{table}_count'] = count
|
|
|
|
# Get active entries (not expired)
|
|
cursor.execute(f"SELECT COUNT(*) FROM {table} WHERE expires_at > datetime('now')")
|
|
active_count = cursor.fetchone()[0]
|
|
stats['active_cache_entries'] += active_count
|
|
stats[f'{table}_active'] = active_count
|
|
|
|
def _stats_messages(self, cursor, tables, stats, top_users_window, top_channels_window):
|
|
"""Message statistics, top users and top channels."""
|
|
if 'message_stats' in tables:
|
|
cursor.execute("SELECT COUNT(*) FROM message_stats")
|
|
stats['total_messages'] = cursor.fetchone()[0]
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(*) FROM message_stats
|
|
WHERE timestamp > strftime('%s', 'now', '-24 hours')
|
|
""")
|
|
stats['messages_24h'] = cursor.fetchone()[0]
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(DISTINCT sender_id) FROM message_stats
|
|
WHERE timestamp > strftime('%s', 'now', '-24 hours')
|
|
""")
|
|
stats['unique_senders_24h'] = cursor.fetchone()[0]
|
|
|
|
# Total unique users and channels
|
|
cursor.execute("SELECT COUNT(DISTINCT sender_id) FROM message_stats")
|
|
stats['unique_users_total'] = cursor.fetchone()[0]
|
|
|
|
cursor.execute("SELECT COUNT(DISTINCT channel) FROM message_stats WHERE channel IS NOT NULL")
|
|
stats['unique_channels_total'] = cursor.fetchone()[0]
|
|
|
|
# Top users (most frequent message senders) - filter by time window
|
|
if top_users_window == '24h':
|
|
time_filter = "WHERE timestamp > strftime('%s', 'now', '-24 hours')"
|
|
elif top_users_window == '7d':
|
|
time_filter = "WHERE timestamp > strftime('%s', 'now', '-7 days')"
|
|
elif top_users_window == '30d':
|
|
time_filter = "WHERE timestamp > strftime('%s', 'now', '-30 days')"
|
|
else: # 'all'
|
|
time_filter = ""
|
|
|
|
query = f"""
|
|
SELECT sender_id, COUNT(*) as count
|
|
FROM message_stats
|
|
{time_filter}
|
|
GROUP BY sender_id
|
|
ORDER BY count DESC
|
|
LIMIT 15
|
|
"""
|
|
cursor.execute(query)
|
|
stats['top_users'] = [{'user': row[0], 'count': row[1]} for row in cursor.fetchall()]
|
|
|
|
# Top channels by message count - filter by time window
|
|
if top_channels_window == '24h':
|
|
time_filter = "AND timestamp > strftime('%s', 'now', '-24 hours')"
|
|
elif top_channels_window == '7d':
|
|
time_filter = "AND timestamp > strftime('%s', 'now', '-7 days')"
|
|
elif top_channels_window == '30d':
|
|
time_filter = "AND timestamp > strftime('%s', 'now', '-30 days')"
|
|
else: # 'all'
|
|
time_filter = ""
|
|
|
|
query = f"""
|
|
SELECT channel, COUNT(*) as message_count, COUNT(DISTINCT sender_id) as unique_users
|
|
FROM message_stats
|
|
WHERE channel IS NOT NULL {time_filter}
|
|
GROUP BY channel
|
|
ORDER BY message_count DESC
|
|
LIMIT 10
|
|
"""
|
|
cursor.execute(query)
|
|
stats['top_channels'] = [
|
|
{'channel': row[0], 'messages': row[1], 'users': row[2]}
|
|
for row in cursor.fetchall()
|
|
]
|
|
|
|
def _stats_commands(self, cursor, tables, stats, top_commands_window):
|
|
"""Command statistics, reply rates and top commands."""
|
|
if 'command_stats' in tables:
|
|
cursor.execute("SELECT COUNT(*) FROM command_stats")
|
|
stats['total_commands'] = cursor.fetchone()[0]
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(*) FROM command_stats
|
|
WHERE timestamp > strftime('%s', 'now', '-24 hours')
|
|
""")
|
|
stats['commands_24h'] = cursor.fetchone()[0]
|
|
|
|
# Top commands - filter by time window
|
|
if top_commands_window == '24h':
|
|
time_filter = "WHERE timestamp > strftime('%s', 'now', '-24 hours')"
|
|
elif top_commands_window == '7d':
|
|
time_filter = "WHERE timestamp > strftime('%s', 'now', '-7 days')"
|
|
elif top_commands_window == '30d':
|
|
time_filter = "WHERE timestamp > strftime('%s', 'now', '-30 days')"
|
|
else: # 'all'
|
|
time_filter = ""
|
|
|
|
query = f"""
|
|
SELECT command_name, COUNT(*) as count
|
|
FROM command_stats
|
|
{time_filter}
|
|
GROUP BY command_name
|
|
ORDER BY count DESC
|
|
LIMIT 15
|
|
"""
|
|
cursor.execute(query)
|
|
stats['top_commands'] = [{'command': row[0], 'count': row[1]} for row in cursor.fetchall()]
|
|
|
|
# Bot reply rates (commands that got responses) - calculate for different time windows
|
|
# 24 hour reply rate
|
|
cursor.execute("""
|
|
SELECT COUNT(*) FROM command_stats
|
|
WHERE timestamp > strftime('%s', 'now', '-24 hours') AND response_sent = 1
|
|
""")
|
|
replied_24h = cursor.fetchone()[0]
|
|
cursor.execute("""
|
|
SELECT COUNT(*) FROM command_stats
|
|
WHERE timestamp > strftime('%s', 'now', '-24 hours')
|
|
""")
|
|
total_24h = cursor.fetchone()[0]
|
|
if total_24h > 0:
|
|
stats['bot_reply_rate_24h'] = round((replied_24h / total_24h) * 100, 1)
|
|
else:
|
|
stats['bot_reply_rate_24h'] = 0
|
|
|
|
# 7 day reply rate
|
|
cursor.execute("""
|
|
SELECT COUNT(*) FROM command_stats
|
|
WHERE timestamp > strftime('%s', 'now', '-7 days') AND response_sent = 1
|
|
""")
|
|
replied_7d = cursor.fetchone()[0]
|
|
cursor.execute("""
|
|
SELECT COUNT(*) FROM command_stats
|
|
WHERE timestamp > strftime('%s', 'now', '-7 days')
|
|
""")
|
|
total_7d = cursor.fetchone()[0]
|
|
if total_7d > 0:
|
|
stats['bot_reply_rate_7d'] = round((replied_7d / total_7d) * 100, 1)
|
|
else:
|
|
stats['bot_reply_rate_7d'] = 0
|
|
|
|
# 30 day reply rate
|
|
cursor.execute("""
|
|
SELECT COUNT(*) FROM command_stats
|
|
WHERE timestamp > strftime('%s', 'now', '-30 days') AND response_sent = 1
|
|
""")
|
|
replied_30d = cursor.fetchone()[0]
|
|
cursor.execute("""
|
|
SELECT COUNT(*) FROM command_stats
|
|
WHERE timestamp > strftime('%s', 'now', '-30 days')
|
|
""")
|
|
total_30d = cursor.fetchone()[0]
|
|
if total_30d > 0:
|
|
stats['bot_reply_rate_30d'] = round((replied_30d / total_30d) * 100, 1)
|
|
else:
|
|
stats['bot_reply_rate_30d'] = 0
|
|
|
|
def _stats_paths(self, cursor, tables, stats, top_paths_window):
|
|
"""Path statistics and top paths."""
|
|
if 'path_stats' in tables:
|
|
cursor.execute("""
|
|
SELECT sender_id, path_length, path_string, timestamp
|
|
FROM path_stats
|
|
ORDER BY path_length DESC
|
|
LIMIT 1
|
|
""")
|
|
longest_path = cursor.fetchone()
|
|
if longest_path:
|
|
stats['longest_path'] = {
|
|
'user': longest_path[0],
|
|
'path_length': longest_path[1],
|
|
'path_string': longest_path[2],
|
|
'timestamp': longest_path[3]
|
|
}
|
|
|
|
# Top paths (longest paths) - filter by time window
|
|
if top_paths_window == '24h':
|
|
time_filter = "WHERE timestamp > strftime('%s', 'now', '-24 hours')"
|
|
elif top_paths_window == '7d':
|
|
time_filter = "WHERE timestamp > strftime('%s', 'now', '-7 days')"
|
|
elif top_paths_window == '30d':
|
|
time_filter = "WHERE timestamp > strftime('%s', 'now', '-30 days')"
|
|
else: # 'all'
|
|
time_filter = ""
|
|
|
|
query = f"""
|
|
SELECT sender_id, path_length, path_string, timestamp
|
|
FROM path_stats
|
|
{time_filter}
|
|
ORDER BY path_length DESC
|
|
LIMIT 5
|
|
"""
|
|
cursor.execute(query)
|
|
stats['top_paths'] = [
|
|
{
|
|
'user': row[0],
|
|
'path_length': row[1],
|
|
'path_string': row[2],
|
|
'timestamp': row[3]
|
|
}
|
|
for row in cursor.fetchall()
|
|
]
|
|
|
|
def _stats_network_health(self, cursor, tables, stats):
|
|
"""Network health: average signal and SNR."""
|
|
if 'complete_contact_tracking' in tables:
|
|
cursor.execute("""
|
|
SELECT AVG(snr) FROM complete_contact_tracking
|
|
WHERE snr IS NOT NULL AND last_heard > datetime('now', 'localtime', '-24 hours')
|
|
""")
|
|
avg_snr = cursor.fetchone()[0]
|
|
stats['avg_snr_24h'] = round(avg_snr, 1) if avg_snr else 0
|
|
|
|
cursor.execute("""
|
|
SELECT AVG(signal_strength) FROM complete_contact_tracking
|
|
WHERE signal_strength IS NOT NULL AND last_heard > datetime('now', 'localtime', '-24 hours')
|
|
""")
|
|
avg_signal = cursor.fetchone()[0]
|
|
stats['avg_signal_strength_24h'] = round(avg_signal, 1) if avg_signal else 0
|
|
|
|
def _stats_geography(self, cursor, tables, stats):
|
|
"""Geographic distribution: currently tracked contacts heard in the last 30 days."""
|
|
# Normalize country names to avoid duplicates (e.g., "United States" vs "United States of America")
|
|
if 'complete_contact_tracking' in tables:
|
|
cursor.execute("""
|
|
SELECT COUNT(DISTINCT
|
|
CASE
|
|
WHEN country IN ('United States', 'United States of America', 'US', 'USA')
|
|
THEN 'United States'
|
|
ELSE country
|
|
END
|
|
) FROM complete_contact_tracking
|
|
WHERE country IS NOT NULL AND country != ''
|
|
AND last_heard > datetime('now', 'localtime', '-30 days')
|
|
AND is_currently_tracked = 1
|
|
""")
|
|
stats['countries'] = cursor.fetchone()[0]
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(DISTINCT state) FROM complete_contact_tracking
|
|
WHERE state IS NOT NULL AND state != ''
|
|
AND last_heard > datetime('now', 'localtime', '-30 days')
|
|
AND is_currently_tracked = 1
|
|
""")
|
|
stats['states'] = cursor.fetchone()[0]
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(DISTINCT city) FROM complete_contact_tracking
|
|
WHERE city IS NOT NULL AND city != ''
|
|
AND last_heard > datetime('now', 'localtime', '-30 days')
|
|
AND is_currently_tracked = 1
|
|
""")
|
|
stats['cities'] = cursor.fetchone()[0]
|
|
|
|
def _get_database_info(self):
|
|
"""Get comprehensive database information for database page"""
|
|
try:
|
|
with self._db_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Get all tables
|
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
|
table_names = [row[0] for row in cursor.fetchall()]
|
|
|
|
# Filter tables by ALLOWED_TABLES whitelist for security
|
|
table_names = [
|
|
name for name in table_names
|
|
if name in self.ALLOWED_TABLES
|
|
]
|
|
|
|
# Get table information
|
|
tables = []
|
|
total_records = 0
|
|
|
|
for table_name in table_names:
|
|
try:
|
|
# Get record count
|
|
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
|
record_count = cursor.fetchone()[0]
|
|
total_records += record_count
|
|
|
|
# Get table size (approximate)
|
|
cursor.execute(f"PRAGMA table_info({table_name})")
|
|
columns = cursor.fetchall()
|
|
|
|
# Estimate size (rough calculation)
|
|
estimated_size = record_count * len(columns) * 50 # Rough estimate
|
|
size_str = f"{estimated_size:,} bytes" if estimated_size < 1024 else f"{estimated_size/1024:.1f} KB"
|
|
|
|
# Get table description based on name
|
|
description = self._get_table_description(table_name)
|
|
|
|
tables.append({
|
|
'name': table_name,
|
|
'record_count': record_count,
|
|
'size': size_str,
|
|
'description': description
|
|
})
|
|
|
|
except Exception as e:
|
|
self.logger.debug(f"Error getting info for table {table_name}: {e}")
|
|
tables.append({
|
|
'name': table_name,
|
|
'record_count': 0,
|
|
'size': 'Unknown',
|
|
'description': 'Error reading table'
|
|
})
|
|
|
|
# Get database file size
|
|
import os
|
|
try:
|
|
db_size_bytes = os.path.getsize(self.db_path)
|
|
if db_size_bytes < 1024:
|
|
db_size = f"{db_size_bytes} bytes"
|
|
elif db_size_bytes < 1024 * 1024:
|
|
db_size = f"{db_size_bytes/1024:.1f} KB"
|
|
else:
|
|
db_size = f"{db_size_bytes/(1024*1024):.1f} MB"
|
|
except:
|
|
db_size = "Unknown"
|
|
|
|
return {
|
|
'total_tables': len(table_names),
|
|
'total_records': total_records,
|
|
'last_updated': time.strftime('%Y-%m-%d %H:%M:%S'),
|
|
'db_size': db_size,
|
|
'tables': tables
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error getting database info: {e}")
|
|
return {
|
|
'total_tables': 0,
|
|
'total_records': 0,
|
|
'last_updated': 'Error',
|
|
'db_size': 'Unknown',
|
|
'tables': []
|
|
}
|
|
|
|
def _is_safe_table_name(self, table_name: str) -> bool:
|
|
"""Check if table name is in the ALLOWED_TABLES whitelist.
|
|
|
|
Args:
|
|
table_name: The table name to validate
|
|
|
|
Returns:
|
|
True if the table is in the allowed whitelist, False otherwise
|
|
"""
|
|
if not table_name or not isinstance(table_name, str):
|
|
return False
|
|
return table_name in self.ALLOWED_TABLES
|
|
|
|
def _get_table_description(self, table_name):
|
|
"""Get human-readable description for table"""
|
|
descriptions = {
|
|
'packet_stream': 'Real-time packet and command data stream',
|
|
'complete_contact_tracking': 'Contact tracking and device information',
|
|
'repeater_contacts': 'Repeater contact management',
|
|
'message_stats': 'Message statistics and analytics',
|
|
'command_stats': 'Command execution statistics',
|
|
'path_stats': 'Network path statistics',
|
|
'geocoding_cache': 'Geocoding service cache',
|
|
'generic_cache': 'General purpose cache storage'
|
|
}
|
|
return descriptions.get(table_name, 'Database table')
|
|
|
|
def _optimize_database(self):
|
|
"""Optimize database using VACUUM, ANALYZE, and REINDEX"""
|
|
try:
|
|
with self._db_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Get initial database size
|
|
import os
|
|
initial_size = os.path.getsize(self.db_path)
|
|
|
|
# Perform VACUUM to reclaim unused space
|
|
self.logger.info("Starting database VACUUM...")
|
|
cursor.execute("VACUUM")
|
|
vacuum_size = os.path.getsize(self.db_path)
|
|
vacuum_saved = initial_size - vacuum_size
|
|
|
|
# Perform ANALYZE to update table statistics
|
|
self.logger.info("Starting database ANALYZE...")
|
|
cursor.execute("ANALYZE")
|
|
|
|
# Get all tables for REINDEX
|
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
tables = [row[0] for row in cursor.fetchall()]
|
|
|
|
# Filter tables by ALLOWED_TABLES whitelist for security
|
|
tables = [t for t in tables if t in self.ALLOWED_TABLES]
|
|
|
|
# Perform REINDEX on all tables
|
|
self.logger.info("Starting database REINDEX...")
|
|
reindexed_tables = []
|
|
for table in tables:
|
|
try:
|
|
cursor.execute(f"REINDEX {table}")
|
|
reindexed_tables.append(table)
|
|
except Exception as e:
|
|
self.logger.debug(f"Could not reindex table {table}: {e}")
|
|
|
|
# Get final database size
|
|
final_size = os.path.getsize(self.db_path)
|
|
total_saved = initial_size - final_size
|
|
|
|
# Format size information
|
|
def format_size(size_bytes):
|
|
if size_bytes < 1024:
|
|
return f"{size_bytes} bytes"
|
|
elif size_bytes < 1024 * 1024:
|
|
return f"{size_bytes/1024:.1f} KB"
|
|
else:
|
|
return f"{size_bytes/(1024*1024):.1f} MB"
|
|
|
|
return {
|
|
'success': True,
|
|
'vacuum_result': f"VACUUM completed - saved {format_size(vacuum_saved)}",
|
|
'analyze_result': f"ANALYZE completed - updated statistics for {len(tables)} tables",
|
|
'reindex_result': f"REINDEX completed - rebuilt indexes for {len(reindexed_tables)} tables",
|
|
'initial_size': format_size(initial_size),
|
|
'final_size': format_size(final_size),
|
|
'total_saved': format_size(total_saved),
|
|
'tables_processed': len(tables),
|
|
'tables_reindexed': len(reindexed_tables)
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error optimizing database: {e}")
|
|
return {
|
|
'success': False,
|
|
'error': str(e)
|
|
}
|
|
|
|
def _get_cache_data(self):
|
|
"""Get cache data"""
|
|
try:
|
|
with self._db_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Get cache statistics
|
|
cursor.execute("SELECT COUNT(*) FROM adverts")
|
|
total_adverts = cursor.fetchone()[0]
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(*) FROM adverts
|
|
WHERE timestamp > datetime('now', '-1 hour')
|
|
""")
|
|
recent_adverts = cursor.fetchone()[0]
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(DISTINCT user_id) FROM adverts
|
|
WHERE timestamp > datetime('now', '-24 hours')
|
|
""")
|
|
active_users = cursor.fetchone()[0]
|
|
|
|
return {
|
|
'total_adverts': total_adverts,
|
|
'recent_adverts_1h': recent_adverts,
|
|
'active_users_24h': active_users,
|
|
'timestamp': time.time()
|
|
}
|
|
except Exception as e:
|
|
self.logger.error(f"Error getting cache data: {e}")
|
|
return {'error': str(e)}
|