cite-agent 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cite-agent might be problematic. Click here for more details.
- cite_agent/__distribution__.py +7 -0
- cite_agent/__init__.py +66 -0
- cite_agent/account_client.py +130 -0
- cite_agent/agent_backend_only.py +172 -0
- cite_agent/ascii_plotting.py +296 -0
- cite_agent/auth.py +281 -0
- cite_agent/backend_only_client.py +83 -0
- cite_agent/cli.py +512 -0
- cite_agent/cli_enhanced.py +207 -0
- cite_agent/dashboard.py +339 -0
- cite_agent/enhanced_ai_agent.py +172 -0
- cite_agent/rate_limiter.py +298 -0
- cite_agent/setup_config.py +417 -0
- cite_agent/telemetry.py +85 -0
- cite_agent/ui.py +175 -0
- cite_agent/updater.py +187 -0
- cite_agent/web_search.py +203 -0
- cite_agent-1.0.0.dist-info/METADATA +234 -0
- cite_agent-1.0.0.dist-info/RECORD +23 -0
- cite_agent-1.0.0.dist-info/WHEEL +5 -0
- cite_agent-1.0.0.dist-info/entry_points.txt +3 -0
- cite_agent-1.0.0.dist-info/licenses/LICENSE +21 -0
- cite_agent-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Nocturnal Archive CLI - Simple, Clean, Functional
|
|
4
|
+
Like Cursor/Claude - no clutter
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import asyncio
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from .enhanced_ai_agent import EnhancedNocturnalAgent, ChatRequest
|
|
13
|
+
from .ui import NocturnalUI, console
|
|
14
|
+
from .auth import AuthManager
|
|
15
|
+
from .setup_config import NocturnalConfig
|
|
16
|
+
from .telemetry import TelemetryManager
|
|
17
|
+
|
|
18
|
+
class NocturnalCLI:
|
|
19
|
+
"""Simple CLI - no bloat"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.agent: Optional[EnhancedNocturnalAgent] = None
|
|
23
|
+
self.auth = AuthManager()
|
|
24
|
+
self.session = None
|
|
25
|
+
self.telemetry = None
|
|
26
|
+
self.queries_today = 0
|
|
27
|
+
self.daily_limit = 25
|
|
28
|
+
|
|
29
|
+
async def run(self):
|
|
30
|
+
"""Main entry point"""
|
|
31
|
+
try:
|
|
32
|
+
# Quick welcome
|
|
33
|
+
NocturnalUI.show_welcome()
|
|
34
|
+
|
|
35
|
+
# Check auth
|
|
36
|
+
self.session = self.auth.get_session()
|
|
37
|
+
|
|
38
|
+
if not self.session:
|
|
39
|
+
# Login or register
|
|
40
|
+
if not await self._handle_auth():
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# Initialize agent
|
|
44
|
+
if not await self._init_agent():
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
# Show status
|
|
48
|
+
email = self.session.get("email", "user")
|
|
49
|
+
NocturnalUI.show_status(email, self.queries_today, self.daily_limit)
|
|
50
|
+
|
|
51
|
+
# Start chat loop
|
|
52
|
+
await self._chat_loop()
|
|
53
|
+
|
|
54
|
+
except KeyboardInterrupt:
|
|
55
|
+
console.print("\nš Goodbye!")
|
|
56
|
+
except Exception as e:
|
|
57
|
+
NocturnalUI.show_error(f"Something went wrong: {e}")
|
|
58
|
+
|
|
59
|
+
async def _handle_auth(self) -> bool:
|
|
60
|
+
"""Handle login or registration"""
|
|
61
|
+
choice = console.input("[1] Login [2] Register [3] Exit\n> ").strip()
|
|
62
|
+
|
|
63
|
+
if choice == "1":
|
|
64
|
+
email, password = NocturnalUI.prompt_login()
|
|
65
|
+
try:
|
|
66
|
+
with NocturnalUI.show_thinking():
|
|
67
|
+
self.session = self.auth.login(email, password)
|
|
68
|
+
NocturnalUI.show_success("Logged in!")
|
|
69
|
+
return True
|
|
70
|
+
except Exception as e:
|
|
71
|
+
NocturnalUI.show_error(f"Login failed: {e}")
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
elif choice == "2":
|
|
75
|
+
email, password, license_key = NocturnalUI.prompt_register()
|
|
76
|
+
try:
|
|
77
|
+
with NocturnalUI.show_thinking():
|
|
78
|
+
self.session = self.auth.register(email, password, license_key)
|
|
79
|
+
NocturnalUI.show_success("Account created!")
|
|
80
|
+
return True
|
|
81
|
+
except Exception as e:
|
|
82
|
+
NocturnalUI.show_error(f"Registration failed: {e}")
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
async def _init_agent(self) -> bool:
|
|
88
|
+
"""Initialize the AI agent"""
|
|
89
|
+
try:
|
|
90
|
+
config = NocturnalConfig()
|
|
91
|
+
self.telemetry = TelemetryManager(config)
|
|
92
|
+
self.agent = EnhancedNocturnalAgent(config, self.telemetry)
|
|
93
|
+
return True
|
|
94
|
+
except Exception as e:
|
|
95
|
+
NocturnalUI.show_error(f"Failed to initialize: {e}")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
async def _chat_loop(self):
|
|
99
|
+
"""Main chat loop - polished and responsive"""
|
|
100
|
+
while True:
|
|
101
|
+
try:
|
|
102
|
+
# Get query
|
|
103
|
+
query = NocturnalUI.prompt_query()
|
|
104
|
+
|
|
105
|
+
if not query.strip():
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
# Handle special commands
|
|
109
|
+
if query.lower() in ['exit', 'quit', 'q']:
|
|
110
|
+
console.print("\n[dim]Thanks for using Nocturnal Archive![/dim]\n")
|
|
111
|
+
break
|
|
112
|
+
elif query.lower() == 'clear':
|
|
113
|
+
console.clear()
|
|
114
|
+
email = self.session.get("email", "user")
|
|
115
|
+
NocturnalUI.show_status(email, self.queries_today, self.daily_limit)
|
|
116
|
+
continue
|
|
117
|
+
elif query.lower() == 'logout':
|
|
118
|
+
self.auth.logout()
|
|
119
|
+
NocturnalUI.show_success("Logged out successfully")
|
|
120
|
+
break
|
|
121
|
+
elif query.lower() in ['help', '?']:
|
|
122
|
+
NocturnalUI.show_help()
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
# Check daily limit
|
|
126
|
+
if self.queries_today >= self.daily_limit:
|
|
127
|
+
NocturnalUI.show_error(
|
|
128
|
+
f"You've reached your daily limit of {self.daily_limit} queries.\n"
|
|
129
|
+
"Your limit resets tomorrow. Thanks for using Nocturnal Archive!"
|
|
130
|
+
)
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
# Process query
|
|
134
|
+
with NocturnalUI.show_thinking():
|
|
135
|
+
request = ChatRequest(
|
|
136
|
+
question=query,
|
|
137
|
+
user_id=self.session.get("user_id", "unknown")
|
|
138
|
+
)
|
|
139
|
+
response = await self.agent.process_request(request)
|
|
140
|
+
|
|
141
|
+
# Show response with metadata
|
|
142
|
+
metadata = {
|
|
143
|
+
"tools_used": response.tools_used if response.tools_used else None,
|
|
144
|
+
"sources": f"{len(response.tools_used)} sources" if response.tools_used else None
|
|
145
|
+
}
|
|
146
|
+
NocturnalUI.show_response(response.response, metadata)
|
|
147
|
+
|
|
148
|
+
self.queries_today += 1
|
|
149
|
+
|
|
150
|
+
# Update status in session
|
|
151
|
+
email = self.session.get("email", "user")
|
|
152
|
+
|
|
153
|
+
except KeyboardInterrupt:
|
|
154
|
+
raise
|
|
155
|
+
except Exception as e:
|
|
156
|
+
# Graceful error handling
|
|
157
|
+
NocturnalUI.show_error(
|
|
158
|
+
f"Something went wrong: {str(e)}\n"
|
|
159
|
+
"The error has been logged. Please try rephrasing your question."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Log for developer
|
|
163
|
+
self._log_error(query if 'query' in locals() else "unknown", str(e))
|
|
164
|
+
|
|
165
|
+
def _log_error(self, query: str, error: str):
|
|
166
|
+
"""Simple error logging - just append to file for dev to check"""
|
|
167
|
+
import datetime
|
|
168
|
+
from pathlib import Path
|
|
169
|
+
|
|
170
|
+
log_dir = Path.home() / ".nocturnal_archive"
|
|
171
|
+
log_dir.mkdir(exist_ok=True)
|
|
172
|
+
|
|
173
|
+
log_file = log_dir / "errors.log"
|
|
174
|
+
|
|
175
|
+
timestamp = datetime.datetime.now().isoformat()
|
|
176
|
+
email = self.session.get("email", "unknown")
|
|
177
|
+
|
|
178
|
+
with open(log_file, "a") as f:
|
|
179
|
+
f.write(f"\n--- {timestamp} ---\n")
|
|
180
|
+
f.write(f"User: {email}\n")
|
|
181
|
+
f.write(f"Query: {query}\n")
|
|
182
|
+
f.write(f"Error: {error}\n")
|
|
183
|
+
|
|
184
|
+
def main():
|
|
185
|
+
"""Entry point"""
|
|
186
|
+
parser = argparse.ArgumentParser(description="Nocturnal Archive - Research & Finance Intelligence")
|
|
187
|
+
parser.add_argument("query", nargs="*", help="Single query to run")
|
|
188
|
+
parser.add_argument("--logout", action="store_true", help="Log out")
|
|
189
|
+
parser.add_argument("--version", action="store_true", help="Show version")
|
|
190
|
+
|
|
191
|
+
args = parser.parse_args()
|
|
192
|
+
|
|
193
|
+
if args.logout:
|
|
194
|
+
AuthManager().logout()
|
|
195
|
+
print("Logged out")
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
if args.version:
|
|
199
|
+
print("Nocturnal Archive v1.0.0-beta")
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
# Run CLI
|
|
203
|
+
cli = NocturnalCLI()
|
|
204
|
+
asyncio.run(cli.run())
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
main()
|
cite_agent/dashboard.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Nocturnal Archive Developer Dashboard
|
|
3
|
+
Real-time monitoring and analytics for beta deployment
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from flask import Flask, render_template, jsonify, request
|
|
7
|
+
from flask_cors import CORS
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
from typing import Dict, List
|
|
14
|
+
import sqlite3
|
|
15
|
+
|
|
16
|
+
app = Flask(__name__)
|
|
17
|
+
CORS(app)
|
|
18
|
+
|
|
19
|
+
class DashboardAnalytics:
|
|
20
|
+
"""Analytics engine for the dashboard"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, db_path: str = None):
|
|
23
|
+
self.db_path = db_path or str(Path.home() / ".nocturnal_archive" / "analytics.db")
|
|
24
|
+
self._init_database()
|
|
25
|
+
|
|
26
|
+
def _init_database(self):
|
|
27
|
+
"""Initialize SQLite database"""
|
|
28
|
+
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
|
|
29
|
+
|
|
30
|
+
conn = sqlite3.connect(self.db_path)
|
|
31
|
+
cursor = conn.cursor()
|
|
32
|
+
|
|
33
|
+
# Users table
|
|
34
|
+
cursor.execute("""
|
|
35
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
36
|
+
user_id TEXT PRIMARY KEY,
|
|
37
|
+
email TEXT UNIQUE NOT NULL,
|
|
38
|
+
license_key TEXT,
|
|
39
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
40
|
+
last_active TIMESTAMP,
|
|
41
|
+
total_queries INTEGER DEFAULT 0,
|
|
42
|
+
total_tokens INTEGER DEFAULT 0,
|
|
43
|
+
status TEXT DEFAULT 'active'
|
|
44
|
+
)
|
|
45
|
+
""")
|
|
46
|
+
|
|
47
|
+
# Queries table
|
|
48
|
+
cursor.execute("""
|
|
49
|
+
CREATE TABLE IF NOT EXISTS queries (
|
|
50
|
+
query_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
51
|
+
user_id TEXT NOT NULL,
|
|
52
|
+
query_text TEXT,
|
|
53
|
+
tools_used TEXT,
|
|
54
|
+
tokens_used INTEGER,
|
|
55
|
+
response_time REAL,
|
|
56
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
57
|
+
FOREIGN KEY (user_id) REFERENCES users(user_id)
|
|
58
|
+
)
|
|
59
|
+
""")
|
|
60
|
+
|
|
61
|
+
# Usage stats table (daily aggregates)
|
|
62
|
+
cursor.execute("""
|
|
63
|
+
CREATE TABLE IF NOT EXISTS daily_stats (
|
|
64
|
+
date DATE PRIMARY KEY,
|
|
65
|
+
total_users INTEGER,
|
|
66
|
+
active_users INTEGER,
|
|
67
|
+
total_queries INTEGER,
|
|
68
|
+
total_tokens INTEGER,
|
|
69
|
+
avg_response_time REAL
|
|
70
|
+
)
|
|
71
|
+
""")
|
|
72
|
+
|
|
73
|
+
# Errors table
|
|
74
|
+
cursor.execute("""
|
|
75
|
+
CREATE TABLE IF NOT EXISTS errors (
|
|
76
|
+
error_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
77
|
+
user_id TEXT,
|
|
78
|
+
error_type TEXT,
|
|
79
|
+
error_message TEXT,
|
|
80
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
81
|
+
)
|
|
82
|
+
""")
|
|
83
|
+
|
|
84
|
+
conn.commit()
|
|
85
|
+
conn.close()
|
|
86
|
+
|
|
87
|
+
def record_query(self, user_id: str, query: str, tools: List[str],
|
|
88
|
+
tokens: int, response_time: float):
|
|
89
|
+
"""Record a query"""
|
|
90
|
+
conn = sqlite3.connect(self.db_path)
|
|
91
|
+
cursor = conn.cursor()
|
|
92
|
+
|
|
93
|
+
# Ensure user exists (create if not)
|
|
94
|
+
cursor.execute("""
|
|
95
|
+
INSERT OR IGNORE INTO users (user_id, email, total_queries, total_tokens)
|
|
96
|
+
VALUES (?, ?, 0, 0)
|
|
97
|
+
""", (user_id, f"{user_id}@unknown.dev"))
|
|
98
|
+
|
|
99
|
+
cursor.execute("""
|
|
100
|
+
INSERT INTO queries (user_id, query_text, tools_used, tokens_used, response_time)
|
|
101
|
+
VALUES (?, ?, ?, ?, ?)
|
|
102
|
+
""", (user_id, query, json.dumps(tools), tokens, response_time))
|
|
103
|
+
|
|
104
|
+
# Update user stats
|
|
105
|
+
cursor.execute("""
|
|
106
|
+
UPDATE users
|
|
107
|
+
SET last_active = CURRENT_TIMESTAMP,
|
|
108
|
+
total_queries = total_queries + 1,
|
|
109
|
+
total_tokens = total_tokens + ?
|
|
110
|
+
WHERE user_id = ?
|
|
111
|
+
""", (tokens, user_id))
|
|
112
|
+
|
|
113
|
+
conn.commit()
|
|
114
|
+
conn.close()
|
|
115
|
+
|
|
116
|
+
def get_overview_stats(self) -> Dict:
|
|
117
|
+
"""Get overview statistics"""
|
|
118
|
+
conn = sqlite3.connect(self.db_path)
|
|
119
|
+
cursor = conn.cursor()
|
|
120
|
+
|
|
121
|
+
# Total users
|
|
122
|
+
cursor.execute("SELECT COUNT(*) FROM users")
|
|
123
|
+
total_users = cursor.fetchone()[0]
|
|
124
|
+
|
|
125
|
+
# Active users (last 24h)
|
|
126
|
+
cursor.execute("""
|
|
127
|
+
SELECT COUNT(*) FROM users
|
|
128
|
+
WHERE last_active > datetime('now', '-1 day')
|
|
129
|
+
""")
|
|
130
|
+
active_users = cursor.fetchone()[0]
|
|
131
|
+
|
|
132
|
+
# Today's queries
|
|
133
|
+
cursor.execute("""
|
|
134
|
+
SELECT COUNT(*), SUM(tokens_used), AVG(response_time)
|
|
135
|
+
FROM queries
|
|
136
|
+
WHERE DATE(timestamp) = DATE('now')
|
|
137
|
+
""")
|
|
138
|
+
today_queries, today_tokens, avg_response = cursor.fetchone()
|
|
139
|
+
|
|
140
|
+
# Total queries
|
|
141
|
+
cursor.execute("SELECT COUNT(*), SUM(tokens_used) FROM queries")
|
|
142
|
+
total_queries, total_tokens = cursor.fetchone()
|
|
143
|
+
|
|
144
|
+
conn.close()
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
'total_users': total_users,
|
|
148
|
+
'active_users_24h': active_users,
|
|
149
|
+
'today_queries': today_queries or 0,
|
|
150
|
+
'today_tokens': today_tokens or 0,
|
|
151
|
+
'total_queries': total_queries or 0,
|
|
152
|
+
'total_tokens': total_tokens or 0,
|
|
153
|
+
'avg_response_time': round(avg_response or 0, 2)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def get_user_list(self) -> List[Dict]:
|
|
157
|
+
"""Get list of all users with stats"""
|
|
158
|
+
conn = sqlite3.connect(self.db_path)
|
|
159
|
+
conn.row_factory = sqlite3.Row
|
|
160
|
+
cursor = conn.cursor()
|
|
161
|
+
|
|
162
|
+
cursor.execute("""
|
|
163
|
+
SELECT user_id, email, created_at, last_active,
|
|
164
|
+
total_queries, total_tokens, status
|
|
165
|
+
FROM users
|
|
166
|
+
ORDER BY last_active DESC
|
|
167
|
+
""")
|
|
168
|
+
|
|
169
|
+
users = [dict(row) for row in cursor.fetchall()]
|
|
170
|
+
conn.close()
|
|
171
|
+
|
|
172
|
+
return users
|
|
173
|
+
|
|
174
|
+
def get_query_history(self, limit: int = 100) -> List[Dict]:
|
|
175
|
+
"""Get recent query history"""
|
|
176
|
+
conn = sqlite3.connect(self.db_path)
|
|
177
|
+
conn.row_factory = sqlite3.Row
|
|
178
|
+
cursor = conn.cursor()
|
|
179
|
+
|
|
180
|
+
cursor.execute("""
|
|
181
|
+
SELECT q.*, u.email
|
|
182
|
+
FROM queries q
|
|
183
|
+
JOIN users u ON q.user_id = u.user_id
|
|
184
|
+
ORDER BY q.timestamp DESC
|
|
185
|
+
LIMIT ?
|
|
186
|
+
""", (limit,))
|
|
187
|
+
|
|
188
|
+
queries = [dict(row) for row in cursor.fetchall()]
|
|
189
|
+
conn.close()
|
|
190
|
+
|
|
191
|
+
return queries
|
|
192
|
+
|
|
193
|
+
def get_usage_trends(self, days: int = 7) -> Dict:
|
|
194
|
+
"""Get usage trends over time"""
|
|
195
|
+
conn = sqlite3.connect(self.db_path)
|
|
196
|
+
cursor = conn.cursor()
|
|
197
|
+
|
|
198
|
+
cursor.execute("""
|
|
199
|
+
SELECT DATE(timestamp) as date,
|
|
200
|
+
COUNT(*) as queries,
|
|
201
|
+
SUM(tokens_used) as tokens
|
|
202
|
+
FROM queries
|
|
203
|
+
WHERE timestamp > datetime('now', '-' || ? || ' days')
|
|
204
|
+
GROUP BY DATE(timestamp)
|
|
205
|
+
ORDER BY date
|
|
206
|
+
""", (days,))
|
|
207
|
+
|
|
208
|
+
trends = {
|
|
209
|
+
'dates': [],
|
|
210
|
+
'queries': [],
|
|
211
|
+
'tokens': []
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for row in cursor.fetchall():
|
|
215
|
+
trends['dates'].append(row[0])
|
|
216
|
+
trends['queries'].append(row[1])
|
|
217
|
+
trends['tokens'].append(row[2])
|
|
218
|
+
|
|
219
|
+
conn.close()
|
|
220
|
+
return trends
|
|
221
|
+
|
|
222
|
+
def kill_switch(self, reason: str = "Emergency shutdown"):
|
|
223
|
+
"""Activate kill switch - disable all users"""
|
|
224
|
+
conn = sqlite3.connect(self.db_path)
|
|
225
|
+
cursor = conn.cursor()
|
|
226
|
+
|
|
227
|
+
cursor.execute("""
|
|
228
|
+
UPDATE users SET status = 'disabled'
|
|
229
|
+
WHERE status = 'active'
|
|
230
|
+
""")
|
|
231
|
+
|
|
232
|
+
# Log the kill switch activation
|
|
233
|
+
cursor.execute("""
|
|
234
|
+
INSERT INTO errors (user_id, error_type, error_message)
|
|
235
|
+
VALUES ('SYSTEM', 'KILL_SWITCH', ?)
|
|
236
|
+
""", (reason,))
|
|
237
|
+
|
|
238
|
+
conn.commit()
|
|
239
|
+
affected = cursor.rowcount
|
|
240
|
+
conn.close()
|
|
241
|
+
|
|
242
|
+
return affected
|
|
243
|
+
|
|
244
|
+
def reactivate_users(self):
|
|
245
|
+
"""Reactivate all users"""
|
|
246
|
+
conn = sqlite3.connect(self.db_path)
|
|
247
|
+
cursor = conn.cursor()
|
|
248
|
+
|
|
249
|
+
cursor.execute("""
|
|
250
|
+
UPDATE users SET status = 'active'
|
|
251
|
+
WHERE status = 'disabled'
|
|
252
|
+
""")
|
|
253
|
+
|
|
254
|
+
conn.commit()
|
|
255
|
+
affected = cursor.rowcount
|
|
256
|
+
conn.close()
|
|
257
|
+
|
|
258
|
+
return affected
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# Initialize analytics
|
|
262
|
+
analytics = DashboardAnalytics()
|
|
263
|
+
|
|
264
|
+
# Routes
|
|
265
|
+
@app.route('/')
|
|
266
|
+
def index():
|
|
267
|
+
"""Dashboard home page"""
|
|
268
|
+
return render_template('dashboard.html')
|
|
269
|
+
|
|
270
|
+
@app.route('/api/overview')
|
|
271
|
+
def api_overview():
|
|
272
|
+
"""Get overview statistics"""
|
|
273
|
+
return jsonify(analytics.get_overview_stats())
|
|
274
|
+
|
|
275
|
+
@app.route('/api/users')
|
|
276
|
+
def api_users():
|
|
277
|
+
"""Get user list"""
|
|
278
|
+
return jsonify(analytics.get_user_list())
|
|
279
|
+
|
|
280
|
+
@app.route('/api/queries')
|
|
281
|
+
def api_queries():
|
|
282
|
+
"""Get query history"""
|
|
283
|
+
limit = request.args.get('limit', 100, type=int)
|
|
284
|
+
return jsonify(analytics.get_query_history(limit))
|
|
285
|
+
|
|
286
|
+
@app.route('/api/trends')
|
|
287
|
+
def api_trends():
|
|
288
|
+
"""Get usage trends"""
|
|
289
|
+
days = request.args.get('days', 7, type=int)
|
|
290
|
+
return jsonify(analytics.get_usage_trends(days))
|
|
291
|
+
|
|
292
|
+
@app.route('/api/kill-switch', methods=['POST'])
|
|
293
|
+
def api_kill_switch():
|
|
294
|
+
"""Activate kill switch"""
|
|
295
|
+
data = request.get_json()
|
|
296
|
+
reason = data.get('reason', 'Emergency shutdown')
|
|
297
|
+
|
|
298
|
+
# Verify admin password
|
|
299
|
+
admin_password = data.get('admin_password')
|
|
300
|
+
if admin_password != os.getenv('NOCTURNAL_ADMIN_PASSWORD', 'admin123'):
|
|
301
|
+
return jsonify({'error': 'Unauthorized'}), 403
|
|
302
|
+
|
|
303
|
+
affected = analytics.kill_switch(reason)
|
|
304
|
+
return jsonify({
|
|
305
|
+
'success': True,
|
|
306
|
+
'affected_users': affected,
|
|
307
|
+
'message': f'Kill switch activated. {affected} users disabled.'
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
@app.route('/api/reactivate', methods=['POST'])
|
|
311
|
+
def api_reactivate():
|
|
312
|
+
"""Reactivate all users"""
|
|
313
|
+
data = request.get_json()
|
|
314
|
+
|
|
315
|
+
# Verify admin password
|
|
316
|
+
admin_password = data.get('admin_password')
|
|
317
|
+
if admin_password != os.getenv('NOCTURNAL_ADMIN_PASSWORD', 'admin123'):
|
|
318
|
+
return jsonify({'error': 'Unauthorized'}), 403
|
|
319
|
+
|
|
320
|
+
affected = analytics.reactivate_users()
|
|
321
|
+
return jsonify({
|
|
322
|
+
'success': True,
|
|
323
|
+
'affected_users': affected,
|
|
324
|
+
'message': f'{affected} users reactivated.'
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def run_dashboard(host='0.0.0.0', port=5000, debug=False):
|
|
329
|
+
"""Run the dashboard server"""
|
|
330
|
+
print(f"š Nocturnal Archive Developer Dashboard")
|
|
331
|
+
print(f"š Dashboard: http://localhost:{port}")
|
|
332
|
+
print(f"š Admin password: {os.getenv('NOCTURNAL_ADMIN_PASSWORD', 'admin123')}")
|
|
333
|
+
print()
|
|
334
|
+
|
|
335
|
+
app.run(host=host, port=port, debug=debug)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
if __name__ == '__main__':
|
|
339
|
+
run_dashboard(debug=True)
|