superbrain-server 1.0.7 → 1.0.10

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.
Files changed (36) hide show
  1. package/package.json +2 -2
  2. package/payload/analyzers/__pycache__/__init__.cpython-311.pyc +0 -0
  3. package/payload/analyzers/__pycache__/audio_transcribe.cpython-311.pyc +0 -0
  4. package/payload/analyzers/__pycache__/caption.cpython-311.pyc +0 -0
  5. package/payload/analyzers/__pycache__/music_identifier.cpython-311.pyc +0 -0
  6. package/payload/analyzers/__pycache__/text_analyzer.cpython-311.pyc +0 -0
  7. package/payload/analyzers/__pycache__/visual_analyze.cpython-311.pyc +0 -0
  8. package/payload/analyzers/__pycache__/webpage_analyzer.cpython-311.pyc +0 -0
  9. package/payload/analyzers/__pycache__/youtube_analyzer.cpython-311.pyc +0 -0
  10. package/payload/api.py +46 -4
  11. package/payload/config/backend_id.txt +1 -0
  12. package/payload/config/localtunnel.log +86 -0
  13. package/payload/core/__pycache__/__init__.cpython-311.pyc +0 -0
  14. package/payload/core/__pycache__/category_manager.cpython-311.pyc +0 -0
  15. package/payload/core/__pycache__/database.cpython-311.pyc +0 -0
  16. package/payload/core/__pycache__/link_checker.cpython-311.pyc +0 -0
  17. package/payload/core/__pycache__/model_router.cpython-311.pyc +0 -0
  18. package/payload/core/model_router.py +12 -0
  19. package/payload/instagram/__pycache__/__init__.cpython-311.pyc +0 -0
  20. package/payload/instagram/__pycache__/instagram_downloader.cpython-311.pyc +0 -0
  21. package/payload/instagram/__pycache__/instagram_login.cpython-311.pyc +0 -0
  22. package/payload/start.py +55 -67
  23. package/payload/test_backend.py +241 -0
  24. package/payload/tests/__init__.py +0 -0
  25. package/payload/tests/__pycache__/__init__.cpython-311.pyc +0 -0
  26. package/payload/tests/__pycache__/test_api.cpython-311.pyc +0 -0
  27. package/payload/tests/__pycache__/test_db.cpython-311.pyc +0 -0
  28. package/payload/tests/__pycache__/test_sync_code.cpython-311.pyc +0 -0
  29. package/payload/tests/test_api.py +17 -0
  30. package/payload/tests/test_db.py +22 -0
  31. package/payload/tests/test_sync_code.py +65 -0
  32. package/payload/utils/__pycache__/__init__.cpython-311.pyc +0 -0
  33. package/payload/utils/__pycache__/db_stats.cpython-311.pyc +0 -0
  34. package/payload/utils/__pycache__/manage_token.cpython-311.pyc +0 -0
  35. package/payload/fix.py +0 -66
  36. package/payload/fix2.py +0 -63
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superbrain-server",
3
- "version": "1.0.7",
3
+ "version": "1.0.10",
4
4
  "description": "1-Line Auto-Installer and Server Execution wrapper for SuperBrain",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -22,4 +22,4 @@
22
22
  "bin/",
23
23
  "payload/"
24
24
  ]
25
- }
25
+ }
package/payload/api.py CHANGED
@@ -289,7 +289,15 @@ class AnalysisResponse(BaseModel):
289
289
  @app.get("/")
290
290
  async def root():
291
291
  """API information and health check (no authentication required)"""
292
+
293
+ backend_id = "unknown"
294
+ backend_id_path = get_config_path("backend_id.txt")
295
+ if backend_id_path.exists():
296
+ backend_id = backend_id_path.read_text().strip()
297
+
292
298
  return {
299
+ "backendId": backend_id,
300
+
293
301
  "name": "SuperBrain Instagram Analyzer API",
294
302
  "version": "1.02",
295
303
  "status": "operational",
@@ -561,12 +569,46 @@ async def analyze_instagram(request: AnalyzeRequest, token: str = Depends(verify
561
569
  logger.warning(f"⚠️ [{shortcode}] main.py stderr:\n{stderr[:1000]}")
562
570
 
563
571
  if returncode == 2:
564
- retry_lines = [l.strip() for l in stdout.splitlines() if l.strip().startswith('?')]
565
- retry_msg = retry_lines[-1].replace('?', '').strip() if retry_lines else "API quota exhausted or rate limited. Queued for automatic retry in 24 hours."
566
- logger.info(f"? [{shortcode}] {retry_msg}")
572
+ # main.py detected quota exhaustion and queued item for retry.
573
+ # NOTE: Do NOT remove from queue here main.py already called
574
+ # queue_for_retry() which set status='retry'. Removing would lose it.
575
+ logger.info(f"⏰ [{shortcode}] Quota exhausted — queued for automatic retry")
567
576
  raise HTTPException(
568
577
  status_code=202,
569
- detail=retry_msg
578
+ detail="API quota exhausted. Your request has been queued for automatic retry in 24 hours."
579
+ )
580
+
581
+ if returncode != 0:
582
+ # Extract last meaningful error line from stdout for the error message
583
+ error_lines = [l.strip() for l in stdout.splitlines() if l.strip() and ('❌' in l or 'Error' in l or 'failed' in l.lower())]
584
+ error_detail = error_lines[-1] if error_lines else (stderr.strip()[:200] or "Analysis failed")
585
+ logger.error(f"❌ [{shortcode}] Analysis failed: {error_detail}")
586
+ logger.debug(f"[{shortcode}] stdout tail:\n{stdout[-800:]}")
587
+ raise HTTPException(
588
+ status_code=400,
589
+ detail=error_detail
590
+ )
591
+
592
+ logger.info(f"✅ [{shortcode}] Analysis complete! Fetching from database...")
593
+
594
+ # Get result from database — retry up to 4 times in case the SQLite write
595
+ # hasn't flushed yet (race condition between subprocess write and our read).
596
+ analysis = None
597
+ for _attempt in range(4):
598
+ analysis = db.check_cache(shortcode)
599
+ if analysis:
600
+ if _attempt > 0:
601
+ logger.info(f"🔄 [{shortcode}] Found in database on retry {_attempt}")
602
+ break
603
+ if _attempt < 3:
604
+ logger.warning(f"⏳ [{shortcode}] Not in DB yet (attempt {_attempt+1}/4), retrying in 1s…")
605
+ await asyncio.sleep(1)
606
+
607
+ if not analysis:
608
+ logger.error(f"❌ [{shortcode}] Not found in database after 4 attempts!")
609
+ raise HTTPException(
610
+ status_code=500,
611
+ detail="Analysis completed but result not found in database"
570
612
  )
571
613
 
572
614
  # Filter response
@@ -0,0 +1 @@
1
+ 3f491f84-184a-40ba-9660-b882b881c4d5
@@ -0,0 +1,86 @@
1
+
2
+ ===============================================================================
3
+ Welcome to localhost.run!
4
+
5
+ Follow your favourite reverse tunnel at [https://twitter.com/localhost_run].
6
+
7
+ To set up and manage custom domains go to https://admin.localhost.run/
8
+
9
+ More details on custom domains (and how to enable subdomains of your custom
10
+ domain) at https://localhost.run/docs/custom-domains
11
+
12
+ If you get a permission denied error check the faq for how to connect with a key or
13
+ create a free tunnel without a key at [http://localhost:3000/docs/faq#generating-an-ssh-key].
14
+
15
+ To explore using localhost.run visit the documentation site:
16
+ https://localhost.run/docs/
17
+
18
+ ===============================================================================
19
+
20
+ ** your connection id is 78ac0b8b-a8f4-4185-9da5-a446d15e032f, please mention it if you send me a message about an issue. **
21
+
22
+ authenticated as anonymous user
23
+ 14ebb2ac2ccab3.lhr.life tunneled with tls termination, https://14ebb2ac2ccab3.lhr.life
24
+ create an account and add your key for a longer lasting domain name. see https://localhost.run/docs/forever-free/ for more information.
25
+ Open your tunnel address on your mobile with this QR:
26
+
27
+                            
28
+                            
29
+                            
30
+                            
31
+                            
32
+                            
33
+                            
34
+                            
35
+                            
36
+                            
37
+                            
38
+                            
39
+                            
40
+                            
41
+                            
42
+                            
43
+                            
44
+                            
45
+                            
46
+                            
47
+                            
48
+                            
49
+                            
50
+                            
51
+                            
52
+                            
53
+                            
54
+
55
+ c3f3097e2d129a.lhr.life tunneled with tls termination, https://c3f3097e2d129a.lhr.life
56
+ create an account and add your key for a longer lasting domain name. see https://localhost.run/docs/forever-free/ for more information.
57
+ Open your tunnel address on your mobile with this QR:
58
+
59
+                            
60
+                            
61
+                            
62
+                            
63
+                            
64
+                            
65
+                            
66
+                            
67
+                            
68
+                            
69
+                            
70
+                            
71
+                            
72
+                            
73
+                            
74
+                            
75
+                            
76
+                            
77
+                            
78
+                            
79
+                            
80
+                            
81
+                            
82
+                            
83
+                            
84
+                            
85
+                            
86
+
@@ -663,6 +663,9 @@ class ModelRouter:
663
663
  try:
664
664
  self._refresh_openrouter_models()
665
665
  except Exception as e:
666
+ if "429" in str(e) or "quota" in str(e).lower():
667
+ raise RateLimitError("Quota limit hit")
668
+ raise e
666
669
  print(f"⚠️ OpenRouter auto-refresh error: {e}")
667
670
  time.sleep(OPENROUTER_FREE_CACHE_HOURS * 3600)
668
671
 
@@ -707,6 +710,9 @@ class ModelRouter:
707
710
  resp.raise_for_status()
708
711
  all_models = resp.json().get("data", [])
709
712
  except Exception as e:
713
+ if "429" in str(e) or "quota" in str(e).lower():
714
+ raise RateLimitError("Quota limit hit")
715
+ raise e
710
716
  print(f"⚠️ OpenRouter model discovery failed: {e}")
711
717
  return
712
718
 
@@ -1100,6 +1106,9 @@ class ModelRouter:
1100
1106
  return result
1101
1107
 
1102
1108
  except Exception as e:
1109
+ if "429" in str(e) or "quota" in str(e).lower():
1110
+ raise RateLimitError("Quota limit hit")
1111
+ raise e
1103
1112
  status = 429 if "429" in str(e) else 0
1104
1113
  self._record_failure(key, str(e), status_code=status)
1105
1114
  print(f" ✗ Failed ({type(e).__name__}), trying next …", flush=True)
@@ -1144,6 +1153,9 @@ class ModelRouter:
1144
1153
  return result
1145
1154
 
1146
1155
  except Exception as e:
1156
+ if "429" in str(e) or "quota" in str(e).lower():
1157
+ raise RateLimitError("Quota limit hit")
1158
+ raise e
1147
1159
  status = 429 if "429" in str(e) else 0
1148
1160
  self._record_failure(key, str(e), status_code=status)
1149
1161
  print(f" ✗ Failed ({type(e).__name__}), trying next …", flush=True)
package/payload/start.py CHANGED
@@ -666,7 +666,7 @@ LOCALTUNNEL_ENABLED = BASE_DIR / "config" / "localtunnel_enabled.txt"
666
666
  LOCALTUNNEL_LOG = BASE_DIR / "config" / "localtunnel.log"
667
667
 
668
668
  def setup_remote_access():
669
- h1("Step 6 of 7 — Remote Access (localhost.run / Port Forwarding)")
669
+ h1("Step 6 of 7 — Remote Access (localtunnel / Port Forwarding)")
670
670
 
671
671
  print(f"""
672
672
  The SuperBrain backend runs on {BOLD}port 5000{RESET} on your machine.
@@ -674,10 +674,10 @@ def setup_remote_access():
674
674
 
675
675
  You have two options:
676
676
 
677
- {BOLD}Option A — localhost.run (easiest + free ssh tunnel){RESET}
678
- localhost.run creates a public HTTPS URL seamlessly using SSH.
677
+ {BOLD}Option A — localtunnel (easiest + free){RESET}
678
+ localtunnel creates a public HTTPS URL that tunnels to your local port 5000.
679
679
  No account required.
680
- Official site: {CYAN}https://localhost.run/{RESET}
680
+ Official site: {CYAN}https://theboroer.github.io/localtunnel-www/{RESET}
681
681
 
682
682
  {BOLD}Option B — Your own port forwarding (advanced){RESET}
683
683
  Forward {BOLD}TCP port 5000{RESET} on your router to your machine's local IP.
@@ -693,10 +693,10 @@ def setup_remote_access():
693
693
  the same network. Use your PC's local IP (e.g. 192.168.x.x) in the app.{RESET}
694
694
  """)
695
695
 
696
- choice = ask_yn("Enable localhost.run on startup?", default=True)
696
+ choice = ask_yn("Enable localtunnel on startup?", default=True)
697
697
  if not choice:
698
698
  LOCALTUNNEL_ENABLED.unlink(missing_ok=True)
699
- warn("Skipping localhost.run. Use either your own port forwarding or local WiFi.")
699
+ warn("Skipping localtunnel. Use either your own port forwarding or local WiFi.")
700
700
  info("Remember: set the correct server URL in the mobile app Settings.")
701
701
  return
702
702
 
@@ -711,15 +711,15 @@ def setup_remote_access():
711
711
 
712
712
  After installing, re-run {BOLD}python start.py{RESET}.
713
713
  """)
714
- warn("Skipping localhost.run setup.")
714
+ warn("Skipping localtunnel setup.")
715
715
  return
716
716
 
717
717
  ok("npx binary found")
718
718
  LOCALTUNNEL_ENABLED.parent.mkdir(parents=True, exist_ok=True)
719
719
  LOCALTUNNEL_ENABLED.write_text("enabled")
720
- ok("localhost.run auto-start enabled")
720
+ ok("localtunnel auto-start enabled")
721
721
  nl()
722
- info("localhost.run will be started automatically every time you run start.py.")
722
+ info("localtunnel will be started automatically every time you run start.py.")
723
723
 
724
724
  # ══════════════════════════════════════════════════════════════════════════════
725
725
  # Step 6 — Access Token & Database
@@ -754,95 +754,83 @@ def setup_token_and_db():
754
754
  # ══════════════════════════════════════════════════════════════════════════════
755
755
  # Launch Backend
756
756
  # ══════════════════════════════════════════════════════════════════════════════
757
- def _extract_localhost_run_url(text: str) -> str | None:
758
- """Extract first localhost.run public URL from text."""
757
+ def _extract_localtunnel_url(text: str) -> str | None:
758
+ """Extract first localtunnel public URL from text."""
759
759
  import re
760
- lines = text.splitlines()
761
- for line in reversed(lines):
762
- if "tunneled with tls termination" in line or ".lhr.life" in line or ".localhost.run" in line:
763
- match = re.search(r'(https://[a-zA-Z0-9-]+\.(?:lhr\.life|localhost\.run))', line)
764
- if match:
765
- return match.group(1)
766
- return None
760
+ m = re.search(r"https://[\w.-]+\.loca\.lt\b", text)
761
+ return m.group(0) if m else None
767
762
 
768
763
 
769
- def _find_localhost_run_url_from_log() -> str | None:
764
+ def _find_localtunnel_url_from_log() -> str | None:
770
765
  """Read local tunnel log and return detected public URL if available."""
771
766
  try:
772
767
  if not LOCALTUNNEL_LOG.exists():
773
768
  return None
774
769
  text = LOCALTUNNEL_LOG.read_text(encoding="utf-8", errors="ignore")
775
- return _extract_localhost_run_url(text)
770
+ return _extract_localtunnel_url(text)
776
771
  except Exception:
777
772
  return None
778
773
 
779
774
 
780
- def _stop_localhost_run_processes():
781
- """Stop existing localhost.run processes so only one tunnel remains active."""
775
+ def _stop_localtunnel_processes():
776
+ """Stop existing localtunnel processes so only one tunnel remains active."""
782
777
  try:
783
778
  if IS_WINDOWS:
784
779
  script = (
785
780
  "Get-CimInstance Win32_Process "
786
- "| Where-Object { $_.CommandLine -match 'nokey@localhost.run' } "
781
+ "| Where-Object { $_.CommandLine -match 'localtunnel|\\.loca\\.lt' } "
787
782
  "| ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"
788
783
  )
789
784
  subprocess.run(["powershell", "-NoProfile", "-Command", script], check=False)
790
785
  else:
791
- subprocess.run(["pkill", "-f", "nokey@localhost.run"], check=False)
786
+ subprocess.run(["pkill", "-f", "localtunnel"], check=False)
792
787
  except Exception:
793
788
  pass
794
789
 
795
790
 
796
- def _start_localhost_run(port: int, timeout: int = 25) -> str | None:
797
- """Start localhost.run in the background via SSH and wait for the public URL."""
791
+ def _start_localtunnel(port: int, timeout: int = 25) -> str | None:
792
+ """Start localtunnel in the background and wait for the public URL."""
798
793
  import time as _time
799
794
 
800
- ssh_exec = shutil.which("ssh")
801
- if not ssh_exec:
802
- warn("SSH is required to use localhost.run. Please install OpenSSH.")
795
+ npx_exec = shutil.which("npx") or shutil.which("npx.cmd")
796
+ if not npx_exec:
803
797
  return None
804
798
 
805
- # Clean stale ssh tunnel processes.
806
- _stop_localhost_run_processes()
799
+ # Clean stale localtunnel processes.
800
+ _stop_localtunnel_processes()
807
801
  _time.sleep(0.8)
808
802
 
809
- info("Starting localhost.run SSH tunnel in background …")
803
+ info("Starting localtunnel in background …")
810
804
  try:
811
805
  LOCALTUNNEL_LOG.parent.mkdir(parents=True, exist_ok=True)
812
806
  LOCALTUNNEL_LOG.write_text("")
813
807
 
814
808
  log_handle = open(LOCALTUNNEL_LOG, "a", encoding="utf-8", buffering=1)
815
809
  kwargs = {
810
+ "start_new_session": True,
816
811
  "stdout": log_handle,
817
812
  "stderr": subprocess.STDOUT,
818
- "stdin": subprocess.DEVNULL,
819
813
  "text": True,
820
814
  }
821
- if IS_WINDOWS:
822
- kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
823
-
824
- cmd = [ssh_exec, "-o", "StrictHostKeyChecking=no", "-R", f"80:localhost:{port}", "nokey@localhost.run"]
825
- process = subprocess.Popen(cmd, **kwargs)
826
-
827
- _time.sleep(1.0)
828
- if process.poll() is not None:
829
- warn(f"Could not start localhost.run, exited with {process.returncode}")
830
- return None
831
-
815
+ if IS_WINDOWS and npx_exec.lower().endswith(".cmd"):
816
+ cmd = ["cmd", "/c", npx_exec, "-y", "localtunnel", "--port", str(port)]
817
+ else:
818
+ cmd = [npx_exec, "-y", "localtunnel", "--port", str(port)]
819
+ subprocess.Popen(cmd, **kwargs)
832
820
  except Exception as e:
833
- warn(f"Could not start localhost.run: {e}")
821
+ warn(f"Could not start localtunnel: {e}")
834
822
  return None
835
823
 
836
824
  # Poll log output until URL is emitted.
837
825
  deadline = _time.time() + timeout
838
826
  while _time.time() < deadline:
839
827
  _time.sleep(1)
840
- url = _find_localhost_run_url_from_log()
828
+ url = _find_localtunnel_url_from_log()
841
829
  if url:
842
- ok(f"localhost.run active → {GREEN}{BOLD}{url}{RESET}")
830
+ ok(f"localtunnel active → {GREEN}{BOLD}{url}{RESET}")
843
831
  return url
844
832
 
845
- warn("localhost.run started but URL is not available yet.")
833
+ warn("localtunnel started but URL is not available yet.")
846
834
  info(f"Check tunnel logs in: {LOCALTUNNEL_LOG}")
847
835
  return None
848
836
 
@@ -1214,26 +1202,26 @@ def launch_backend():
1214
1202
  token = TOKEN_FILE.read_text().strip() if TOKEN_FILE.exists() else "—"
1215
1203
  local_ip = _detect_local_ip()
1216
1204
 
1217
- localhost_run_enabled = bool(shutil.which("ssh"))
1205
+ localtunnel_enabled = bool(shutil.which("npx") or shutil.which("npx.cmd"))
1218
1206
 
1219
- localhost_run_url: str | None = None
1220
- if localhost_run_enabled:
1221
- localhost_run_url = _start_localhost_run(PORT)
1207
+ localtunnel_url: str | None = None
1208
+ if localtunnel_enabled:
1209
+ localtunnel_url = _start_localtunnel(PORT)
1222
1210
  else:
1223
- localhost_run_url = _find_localhost_run_url_from_log()
1211
+ localtunnel_url = _find_localtunnel_url_from_log()
1224
1212
 
1225
- if localhost_run_url:
1226
- tunnel_line = f" Public URL → {GREEN}{BOLD}{localhost_run_url}{RESET} {DIM}(localhost.run){RESET}"
1227
- tunnel_hint = f" · public → {GREEN}{localhost_run_url}{RESET}"
1228
- elif localhost_run_enabled:
1213
+ if localtunnel_url:
1214
+ tunnel_line = f" Public URL → {GREEN}{BOLD}{localtunnel_url}{RESET} {DIM}(localtunnel){RESET}"
1215
+ tunnel_hint = f" · public → {GREEN}{localtunnel_url}{RESET}"
1216
+ elif localtunnel_enabled:
1229
1217
  tunnel_line = f" Public URL → {YELLOW}(starting — URL pending, check localtunnel.log){RESET}"
1230
- tunnel_hint = f" · public → run: {DIM}ssh -R 80:localhost:{PORT} nokey@localhost.run{RESET}"
1218
+ tunnel_hint = f" · public → run: {DIM}npx localtunnel --port {PORT}{RESET}"
1231
1219
  else:
1232
1220
  tunnel_line = ""
1233
- tunnel_hint = f" · public → install OpenSSH first, then run: {DIM}ssh -R 80:localhost:{PORT} nokey@localhost.run{RESET}"
1221
+ tunnel_hint = f" · public → install Node.js first, then run: {DIM}npx localtunnel --port {PORT}{RESET}"
1234
1222
 
1235
1223
  # ── Generate and display QR code ──────────────────────────────────────────
1236
- qr_url = localhost_run_url if localhost_run_url else f"http://{local_ip}:{PORT}"
1224
+ qr_url = localtunnel_url if localtunnel_url else f"http://{local_ip}:{PORT}"
1237
1225
  _display_connect_qr(qr_url, token)
1238
1226
 
1239
1227
  print(f"""
@@ -1294,7 +1282,7 @@ def launch_backend_status():
1294
1282
  url = match.group(1)
1295
1283
 
1296
1284
  if url == "NOT_FOUND":
1297
- warn("Could not find a running localhost.run URL in config/localtunnel.log.")
1285
+ warn("Could not find a running localtunnel URL in config/localtunnel.log.")
1298
1286
  nl()
1299
1287
  print(" Wait 5 seconds, or run 'superbrain-server' to start the server.")
1300
1288
  return
@@ -1306,11 +1294,11 @@ def launch_backend_status():
1306
1294
  network_url = f"http://{local_ip}:5000"
1307
1295
 
1308
1296
  nl()
1309
- print(f" Local URL ? {CYAN}{local_url}{RESET}")
1310
- print(f" Network URL ? {CYAN}{network_url}{RESET}")
1311
- print(f" Public URL ? {CYAN}{url}{RESET} (localtunnel)")
1312
- print(f" API docs ? {CYAN}{local_url}/docs{RESET}")
1313
- print(f" Access Token ? {BOLD}{MAGENTA}{token}{RESET}")
1297
+ print(f" Local URL \u2192 {CYAN}{local_url}{RESET}")
1298
+ print(f" Network URL \u2192 {CYAN}{network_url}{RESET}")
1299
+ print(f" Public URL \u2192 {CYAN}{url}{RESET} (localtunnel)")
1300
+ print(f" API docs \u2192 {CYAN}{local_url}/docs{RESET}")
1301
+ print(f" Access Token \u2192 {BOLD}{MAGENTA}{token}{RESET}")
1314
1302
  nl()
1315
1303
 
1316
1304
  def main():
@@ -1339,7 +1327,7 @@ def main():
1339
1327
  3 · Configure AI provider keys + Instagram credentials
1340
1328
  4 · Set up an offline AI model via Ollama (qwen3-vl:4b)
1341
1329
  5 · Set up offline audio transcription (Whisper + ffmpeg)
1342
- 6 · Configure remote access (localhost.run or port forwarding)
1330
+ 6 · Configure remote access (localtunnel or port forwarding)
1343
1331
  7 · Generate Access Token & initialise database
1344
1332
 
1345
1333
  Press {BOLD}Enter{RESET} to accept defaults shown in [{DIM}brackets{RESET}].
@@ -0,0 +1,241 @@
1
+ """
2
+ SuperBrain - Comprehensive Backend Test Suite
3
+ Tests all API endpoints against the running backend.
4
+ """
5
+ import requests
6
+ import sys
7
+ import json
8
+ import time
9
+
10
+ # Config
11
+ BASE_URL = "http://localhost:5000"
12
+ TOKEN = "4XVTLWWV"
13
+ HEADERS = {"X-API-Key": TOKEN, "Content-Type": "application/json"}
14
+
15
+ TEST_YT_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
16
+
17
+ passed = 0
18
+ failed = 0
19
+ skipped = 0
20
+
21
+ def test(name, condition, detail=""):
22
+ global passed, failed
23
+ if condition:
24
+ passed += 1
25
+ print(f" [PASS] {name}")
26
+ else:
27
+ failed += 1
28
+ print(f" [FAIL] {name} -- {detail}")
29
+
30
+ def skip(name, reason):
31
+ global skipped
32
+ skipped += 1
33
+ print(f" [SKIP] {name} -- {reason}")
34
+
35
+ # Phase 1: Health & Auth
36
+ print("\n--- Phase 1: Health & Auth ---")
37
+
38
+ try:
39
+ r = requests.get(f"{BASE_URL}/status", timeout=5)
40
+ test("1.1 GET /status", r.status_code == 200 and r.json().get("status") == "online", f"status={r.status_code}")
41
+ except Exception as e:
42
+ test("1.1 GET /status", False, str(e))
43
+
44
+ try:
45
+ r = requests.get(f"{BASE_URL}/health", headers=HEADERS, timeout=5)
46
+ data = r.json()
47
+ test("1.2 GET /health (auth)", r.status_code == 200 and "database" in data, f"status={r.status_code}")
48
+ except Exception as e:
49
+ test("1.2 GET /health", False, str(e))
50
+
51
+ try:
52
+ r = requests.get(f"{BASE_URL}/health", headers={"X-API-Key": "WRONGTKN"}, timeout=5)
53
+ test("1.3 Auth rejection (wrong)", r.status_code == 401, f"status={r.status_code}")
54
+ except Exception as e:
55
+ test("1.3 Auth rejection", False, str(e))
56
+
57
+ try:
58
+ r = requests.get(f"{BASE_URL}/health", timeout=5)
59
+ test("1.4 Auth rejection (none)", r.status_code in [401, 403, 422], f"status={r.status_code}")
60
+ except Exception as e:
61
+ test("1.4 Auth rejection (none)", False, str(e))
62
+
63
+ # Phase 2: Read Endpoints
64
+ print("\n--- Phase 2: Read Endpoints ---")
65
+
66
+ try:
67
+ r = requests.get(f"{BASE_URL}/recent?limit=10", headers=HEADERS, timeout=10)
68
+ data = r.json()
69
+ test("2.1 GET /recent", r.status_code == 200 and "data" in data, f"status={r.status_code}")
70
+ print(f" found {len(data.get('data', []))} posts")
71
+ except Exception as e:
72
+ test("2.1 GET /recent", False, str(e))
73
+
74
+ try:
75
+ r = requests.get(f"{BASE_URL}/categories", headers=HEADERS, timeout=10)
76
+ data = r.json()
77
+ test("2.2 GET /categories", r.status_code == 200 and "categories" in data, f"status={r.status_code}")
78
+ print(f" found {len(data.get('categories', []))} categories")
79
+ except Exception as e:
80
+ test("2.2 GET /categories", False, str(e))
81
+
82
+ try:
83
+ r = requests.get(f"{BASE_URL}/queue-status", headers=HEADERS, timeout=10)
84
+ data = r.json()
85
+ test("2.3 GET /queue-status", r.status_code == 200 and "processing_count" in data, f"keys={list(data.keys())}")
86
+ print(f" processing={data.get('processing_count',0)}, queue={data.get('queue_count',0)}, retry={data.get('retry_count',0)}")
87
+ except Exception as e:
88
+ test("2.3 GET /queue-status", False, str(e))
89
+
90
+ try:
91
+ r = requests.get(f"{BASE_URL}/stats", headers=HEADERS, timeout=10)
92
+ test("2.4 GET /stats", r.status_code == 200, f"status={r.status_code}")
93
+ except Exception as e:
94
+ test("2.4 GET /stats", False, str(e))
95
+
96
+ try:
97
+ r = requests.get(f"{BASE_URL}/search?tags=travel&limit=5", headers=HEADERS, timeout=10)
98
+ test("2.5 GET /search?tags=travel", r.status_code == 200, f"status={r.status_code}")
99
+ except Exception as e:
100
+ test("2.5 GET /search", False, str(e))
101
+
102
+ try:
103
+ r = requests.get(f"{BASE_URL}/queue/retry", headers=HEADERS, timeout=10)
104
+ test("2.6 GET /queue/retry", r.status_code == 200, f"status={r.status_code}")
105
+ except Exception as e:
106
+ test("2.6 GET /queue/retry", False, str(e))
107
+
108
+ # Phase 3: Collections CRUD
109
+ print("\n--- Phase 3: Collections CRUD ---")
110
+
111
+ test_collection_id = None
112
+
113
+ try:
114
+ r = requests.get(f"{BASE_URL}/collections", headers=HEADERS, timeout=10)
115
+ data = r.json()
116
+ test("3.1 GET /collections", r.status_code == 200, f"status={r.status_code}")
117
+ print(f" found {len(data)} collections")
118
+ except Exception as e:
119
+ test("3.1 GET /collections", False, str(e))
120
+
121
+ try:
122
+ r = requests.post(f"{BASE_URL}/collections", headers=HEADERS, json={"id": "test-id", "name": "Test Collection", "icon": "test"}, timeout=10)
123
+ data = r.json()
124
+ test("3.2 POST /collections (create)", r.status_code == 200 and data.get("id"), f"status={r.status_code}")
125
+ test_collection_id = data.get("id")
126
+ print(f" created ID: {test_collection_id}")
127
+ except Exception as e:
128
+ test("3.2 POST /collections", False, str(e))
129
+
130
+ if test_collection_id:
131
+ try:
132
+ r = requests.post(f"{BASE_URL}/collections", headers=HEADERS, json={"id": test_collection_id, "name": "Updated Test", "icon": "ok"}, timeout=10)
133
+ test("3.3 POST /collections (update)", r.status_code == 200, f"status={r.status_code}")
134
+ except Exception as e:
135
+ test("3.3 POST /collections (update)", False, str(e))
136
+
137
+ try:
138
+ r = requests.delete(f"{BASE_URL}/collections/{test_collection_id}", headers=HEADERS, timeout=10)
139
+ test("3.4 DELETE /collections", r.status_code == 200, f"status={r.status_code}")
140
+ except Exception as e:
141
+ test("3.4 DELETE /collections", False, str(e))
142
+ else:
143
+ skip("3.3 PUT /collections", "no collection created")
144
+ skip("3.4 DELETE /collections", "no collection created")
145
+
146
+ try:
147
+ r = requests.get(f"{BASE_URL}/collections", headers=HEADERS, timeout=10)
148
+ data = r.json()
149
+ collections = data.get("data", [])
150
+ wl = next((c for c in collections if c.get("name") == "Watch Later"), None)
151
+ if wl:
152
+ r = requests.delete(f"{BASE_URL}/collections/{wl['id']}", headers=HEADERS, timeout=10)
153
+ test("3.5 Watch Later protection", r.status_code in [400, 403], f"status={r.status_code} (should be blocked)")
154
+ else:
155
+ skip("3.5 Watch Later protection", "no Watch Later found")
156
+ except Exception as e:
157
+ test("3.5 Watch Later protection", False, str(e))
158
+
159
+ # Phase 4: Settings
160
+ print("\n--- Phase 4: Settings ---")
161
+
162
+ try:
163
+ r = requests.get(f"{BASE_URL}/settings/ai-providers", headers=HEADERS, timeout=10)
164
+ test("4.1 GET /settings/ai-providers", r.status_code == 200, f"status={r.status_code}")
165
+ except Exception as e:
166
+ test("4.1 GET /settings/ai-providers", False, str(e))
167
+
168
+ try:
169
+ r = requests.post(f"{BASE_URL}/settings/ai-providers", headers=HEADERS,
170
+ json={"provider": "groq", "api_key": "gsk_test"}, timeout=10)
171
+ test("4.2 POST ai-providers (Groq)", r.status_code == 200, f"status={r.status_code}")
172
+ except Exception as e:
173
+ test("4.2 POST ai-providers", False, str(e))
174
+
175
+ try:
176
+ r = requests.get(f"{BASE_URL}/settings/instagram", headers=HEADERS, timeout=10)
177
+ test("4.3 GET /settings/instagram", r.status_code == 200, f"status={r.status_code}")
178
+ except Exception as e:
179
+ test("4.3 GET /settings/instagram", False, str(e))
180
+
181
+ try:
182
+ r = requests.post(f"{BASE_URL}/settings/instagram", headers=HEADERS,
183
+ json={"username": "testuser", "password": "testpass"}, timeout=10)
184
+ test("4.4 POST /settings/instagram", r.status_code == 200, f"status={r.status_code}")
185
+ except Exception as e:
186
+ test("4.4 POST /settings/instagram", False, str(e))
187
+
188
+ # Phase 5: Export/Import
189
+ print("\n--- Phase 5: Export/Import ---")
190
+
191
+ try:
192
+ r = requests.get(f"{BASE_URL}/export", headers=HEADERS, timeout=15)
193
+ test("5.1 GET /export", r.status_code == 200, f"status={r.status_code}")
194
+ ed = r.json()
195
+ print(f" exported {len(ed.get('posts', []))} posts, {len(ed.get('collections', []))} collections")
196
+ except Exception as e:
197
+ test("5.1 GET /export", False, str(e))
198
+
199
+ try:
200
+ r = requests.post(f"{BASE_URL}/import", headers=HEADERS,
201
+ json={"posts": [], "collections": [], "mode": "merge"}, timeout=15)
202
+ test("5.2 POST /import (merge, empty)", r.status_code == 200, f"status={r.status_code}")
203
+ except Exception as e:
204
+ test("5.2 POST /import", False, str(e))
205
+
206
+ # Phase 6: Analysis
207
+ print("\n--- Phase 6: Analysis (YouTube) ---")
208
+
209
+ try:
210
+ print(" submitting YouTube URL... (timeout 90s)")
211
+ r = requests.post(f"{BASE_URL}/analyze", headers=HEADERS,
212
+ json={"url": TEST_YT_URL}, timeout=90)
213
+ data = r.json()
214
+ if r.status_code == 200:
215
+ test("6.1 POST /analyze (YouTube)", data.get("success") == True, f"success={data.get('success')}")
216
+ if data.get("data"):
217
+ d = data["data"]
218
+ print(f" title: {d.get('title', 'N/A')[:60]}")
219
+ print(f" category: {d.get('category', 'N/A')}")
220
+ print(f" tags: {str(d.get('tags', [])[:5])}")
221
+ print(f" cached: {data.get('cached', False)}")
222
+ elif r.status_code == 202:
223
+ skip("6.1 POST /analyze (YouTube)", "queued for retry (quota)")
224
+ elif r.status_code == 503:
225
+ skip("6.1 POST /analyze (YouTube)", "503 server busy")
226
+ else:
227
+ test("6.1 POST /analyze (YouTube)", False, f"status={r.status_code}")
228
+ except requests.exceptions.Timeout:
229
+ skip("6.1 POST /analyze (YouTube)", "timed out (90s)")
230
+ except Exception as e:
231
+ test("6.1 POST /analyze (YouTube)", False, str(e))
232
+
233
+ # Summary
234
+ print("\n" + "=" * 50)
235
+ print(f" PASSED: {passed}")
236
+ print(f" FAILED: {failed}")
237
+ print(f" SKIPPED: {skipped}")
238
+ print(f" TOTAL: {passed + failed + skipped}")
239
+ print("=" * 50)
240
+
241
+ sys.exit(1 if failed > 0 else 0)
File without changes
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env python3
2
+ import requests
3
+ import json
4
+
5
+ # Read token
6
+ with open('token.txt', 'r') as f:
7
+ token = f.read().strip()
8
+
9
+ # Test the /recent endpoint
10
+ response = requests.get(
11
+ 'http://localhost:5000/recent?limit=10',
12
+ headers={'X-API-Key': token}
13
+ )
14
+
15
+ print(f"Status Code: {response.status_code}")
16
+ print(f"\nResponse:")
17
+ print(json.dumps(response.json(), indent=2))
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env python3
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ # Ensure backend root is in sys.path
6
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
7
+
8
+ from core.database import get_db
9
+
10
+ db = get_db()
11
+ posts = db.get_recent(5)
12
+ print(f'Found {len(posts)} posts in database')
13
+ print()
14
+
15
+ for p in posts:
16
+ print(f"Shortcode: {p.get('shortcode', 'N/A')}")
17
+ print(f"Title: {p.get('title', 'N/A')}")
18
+ print(f"Username: {p.get('username', 'N/A')}")
19
+ print(f"URL: {p.get('url', 'N/A')}")
20
+ print(f"Category: {p.get('category', 'N/A')}")
21
+ print(f"Analyzed: {p.get('analyzed_at', 'N/A')}")
22
+ print("-" * 60)
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Legacy filename retained for compatibility.
4
+ This script now validates API key authentication behavior.
5
+ """
6
+
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from fastapi.testclient import TestClient
11
+
12
+ # Ensure backend root is in sys.path
13
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
14
+
15
+ from api import app, API_TOKEN, generate_api_token # noqa: E402
16
+
17
+
18
+ client = TestClient(app)
19
+
20
+
21
+ def test_generate_api_token_shape():
22
+ token = generate_api_token()
23
+ assert isinstance(token, str)
24
+ assert len(token) == 8
25
+ assert token.isalnum()
26
+
27
+
28
+ def test_ping_works_without_auth():
29
+ response = client.get('/ping')
30
+ assert response.status_code == 200
31
+
32
+
33
+ def test_queue_status_rejects_invalid_api_key():
34
+ response = client.get('/queue-status', headers={'X-API-Key': 'INVALID_TOKEN'})
35
+ assert response.status_code == 401
36
+
37
+
38
+ def test_queue_status_accepts_valid_api_key():
39
+ response = client.get('/queue-status', headers={'X-API-Key': API_TOKEN})
40
+ assert response.status_code == 200
41
+
42
+
43
+ def test_connect_endpoint_is_deprecated():
44
+ response = client.post('/connect', json={'api_key': API_TOKEN})
45
+ assert response.status_code == 410
46
+
47
+
48
+ if __name__ == '__main__':
49
+ print('Running API key auth tests...')
50
+ test_generate_api_token_shape()
51
+ print('PASS: API token generation shape')
52
+
53
+ test_ping_works_without_auth()
54
+ print('PASS: /ping unauthenticated access')
55
+
56
+ test_queue_status_rejects_invalid_api_key()
57
+ print('PASS: /queue-status rejects invalid API key')
58
+
59
+ test_queue_status_accepts_valid_api_key()
60
+ print('PASS: /queue-status accepts valid API key')
61
+
62
+ test_connect_endpoint_is_deprecated()
63
+ print('PASS: /connect deprecated behavior')
64
+
65
+ print('\nAll API key auth tests passed!')
package/payload/fix.py DELETED
@@ -1,66 +0,0 @@
1
- import sys, shutil
2
- path1 = 'D:/superbrain/backend/start.py'
3
- path2 = 'D:/superbrain/superbrain-cli/payload/start.py'
4
-
5
- status_code = '''
6
- def launch_backend_status():
7
- h1("SuperBrain Server Status")
8
- PORT = 5000
9
- token = TOKEN_FILE.read_text().strip() if TOKEN_FILE.exists() else "�"
10
- local_ip = _detect_local_ip()
11
-
12
- localtunnel_enabled = bool(shutil.which("npx") or shutil.which("npx.cmd"))
13
- localtunnel_url: str | None = None
14
- if localtunnel_enabled:
15
- localtunnel_url = _find_localtunnel_url_from_log()
16
-
17
- if localtunnel_url:
18
- tunnel_line = f" Public URL ? {GREEN}{BOLD}{localtunnel_url}{RESET} {DIM}(localtunnel){RESET}"
19
- tunnel_hint = f" � public ? {GREEN}{localtunnel_url}{RESET}"
20
- elif localtunnel_enabled:
21
- tunnel_line = f" Public URL ? {YELLOW}(running � URL in localtunnel.log){RESET}"
22
- tunnel_hint = f" � public ? run: {DIM}npx localtunnel --port {PORT}{RESET}"
23
- else:
24
- tunnel_line = ""
25
- tunnel_hint = f" � public ? install Node.js first, then run: {DIM}npx localtunnel --port {PORT}{RESET}"
26
-
27
- qr_url = localtunnel_url if localtunnel_url else f"http://{local_ip}:{PORT}"
28
- _display_connect_qr(qr_url, token)
29
-
30
- print(f\"\"\"
31
- {GREEN}{BOLD}Server Status{RESET}
32
-
33
- Local URL ? {CYAN}http://127.0.0.1:{PORT}{RESET}
34
- Network URL ? {CYAN}http://{local_ip}:{PORT}{RESET}
35
- {(tunnel_line + chr(10)) if tunnel_line else ''} API docs ? {CYAN}http://127.0.0.1:{PORT}/docs{RESET}
36
- Access Token ? {BOLD}{MAGENTA}{token}{RESET}
37
-
38
- {YELLOW}Mobile app setup:{RESET}
39
- {BOLD}Option A � Scan QR code:{RESET}
40
- 1. Open the app ? Settings ? tap the {BOLD}QR icon{RESET} ??
41
- 2. Scan the QR code shown above
42
-
43
- {BOLD}Option B � Manual setup:{RESET}
44
- 1. Go to app ? ? settings
45
- 2. Set {BOLD}Server URL{RESET} to:
46
- {tunnel_hint}
47
- � Same WiFi ? http://{local_ip}:{PORT}
48
- 3. Set {BOLD}Access Token{RESET} to: {BOLD}{MAGENTA}{token}{RESET}
49
- \"\"\")
50
- sys.exit(0)
51
-
52
- '''
53
-
54
- for path in [path1, path2]:
55
- with open(path, 'r', encoding='utf-8') as f:
56
- content = f.read()
57
-
58
- if 'def launch_backend_status()' not in content:
59
- content = content.replace('def main():', status_code + 'def main():')
60
-
61
- if 'status_mode = "--status" in sys.argv' not in content:
62
- content = content.replace(' reset_mode = "--reset" in sys.argv', ' status_mode = "--status" in sys.argv\\n if status_mode:\\n launch_backend_status()\\n return\\n\\n reset_mode = "--reset" in sys.argv')
63
-
64
- with open(path, 'w', encoding='utf-8') as f:
65
- f.write(content)
66
- print("Done fixing start.py!")
package/payload/fix2.py DELETED
@@ -1,63 +0,0 @@
1
- import sys, shutil
2
- path1 = 'D:/superbrain/backend/start.py'
3
- path2 = 'D:/superbrain/superbrain-cli/payload/start.py'
4
-
5
- status_code = '''
6
- def launch_backend_status():
7
- h1("SuperBrain Server Status")
8
- PORT = 5000
9
- token = TOKEN_FILE.read_text().strip() if TOKEN_FILE.exists() else "�"
10
- local_ip = _detect_local_ip()
11
-
12
- localtunnel_enabled = bool(shutil.which("npx") or shutil.which("npx.cmd"))
13
- localtunnel_url: str | None = None
14
- if localtunnel_enabled:
15
- localtunnel_url = _find_localtunnel_url_from_log()
16
-
17
- if localtunnel_url:
18
- tunnel_line = f" Public URL ? {GREEN}{BOLD}{localtunnel_url}{RESET} {DIM}(localtunnel){RESET}"
19
- tunnel_hint = f" � public ? {GREEN}{localtunnel_url}{RESET}"
20
- elif localtunnel_enabled:
21
- tunnel_line = f" Public URL ? {YELLOW}(running � URL in localtunnel.log){RESET}"
22
- tunnel_hint = f" � public ? run: {DIM}npx localtunnel --port {PORT}{RESET}"
23
- else:
24
- tunnel_line = ""
25
- tunnel_hint = f" � public ? install Node.js first, then run: {DIM}npx localtunnel --port {PORT}{RESET}"
26
-
27
- qr_url = localtunnel_url if localtunnel_url else f"http://{local_ip}:{PORT}"
28
- _display_connect_qr(qr_url, token)
29
-
30
- print(f\"\"\"
31
- {GREEN}{BOLD}Server Status{RESET}
32
-
33
- Local URL ? {CYAN}http://127.0.0.1:{PORT}{RESET}
34
- Network URL ? {CYAN}http://{local_ip}:{PORT}{RESET}
35
- {(tunnel_line + chr(10)) if tunnel_line else ''} API docs ? {CYAN}http://127.0.0.1:{PORT}/docs{RESET}
36
- Access Token ? {BOLD}{MAGENTA}{token}{RESET}
37
-
38
- {YELLOW}Mobile app setup:{RESET}
39
- {BOLD}Option A � Scan QR code:{RESET}
40
- 1. Open the app ? Settings ? tap the {BOLD}QR icon{RESET} ??
41
- 2. Scan the QR code shown above
42
-
43
- {BOLD}Option B � Manual setup:{RESET}
44
- 1. Go to app ? ? settings
45
- 2. Set {BOLD}Server URL{RESET} to:
46
- {tunnel_hint}
47
- � Same WiFi ? http://{local_ip}:{PORT}
48
- 3. Set {BOLD}Access Token{RESET} to: {BOLD}{MAGENTA}{token}{RESET}
49
- \"\"\")
50
- sys.exit(0)
51
-
52
- '''
53
-
54
- for path in [path1, path2]:
55
- with open(path, 'r', encoding='utf-8') as f:
56
- content = f.read()
57
-
58
- if 'def launch_backend_status()' not in content:
59
- content = content.replace('def main():', status_code + 'def main():')
60
-
61
- with open(path, 'w', encoding='utf-8') as f:
62
- f.write(content)
63
- print("Done fixing start.py again!")