chore: reorganize into polyglot monorepo (workshop)
- Move bigmind/ -> mcp/bigmind/ - Move webscraper/ -> mcp/webscraper/ - Move mss-failsafe/ -> java/mss-failsafe/ - Move Wellmann-Shop/ -> java/wellmann-shop/ (normalize to kebab-case) - Add .roo/ IDE config files to tracking - Add plans/REPO_STRATEGY.md (monorepo strategy document) - Expand .gitignore: Java/Maven, Node/TS, coverage, uv.lock - Rewrite README.md as navigation index - Update .roo/mcp.json webscraper path to mcp/webscraper/
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
"""BigMind Profile Web Server — Flask app served on localhost:BIGMIND_PORT (default 7700).
|
||||
|
||||
Started automatically as a daemon thread when the MCP server starts.
|
||||
Serves a single live profile page built from the BigMind DB.
|
||||
"""
|
||||
|
||||
import os
|
||||
import threading
|
||||
import logging
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
from bigmind.web_render import _render_html # all HTML rendering lives there
|
||||
|
||||
logger = logging.getLogger("BigMindWeb")
|
||||
|
||||
_PORT = int(os.environ.get("BIGMIND_PORT", "7700"))
|
||||
_AUTOOPEN = os.environ.get("BIGMIND_AUTOOPEN", "").lower() in ("1", "true", "yes")
|
||||
_server_started = False
|
||||
|
||||
|
||||
# ── Flask app ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def _create_app():
|
||||
from flask import Flask, jsonify, request
|
||||
from bigmind import memory_store
|
||||
from bigmind.profile_builder import build_profile_data
|
||||
|
||||
app = Flask(__name__)
|
||||
app.logger.setLevel(logging.WARNING) # silence Flask request logs
|
||||
|
||||
@app.route("/")
|
||||
def profile():
|
||||
user = memory_store.get_or_create_user(memory_store.get_current_username())
|
||||
data = build_profile_data(user["id"])
|
||||
return _render_html(data)
|
||||
|
||||
@app.route("/api/session/<session_id>")
|
||||
def api_session(session_id):
|
||||
"""Return Tier-2 summary JSON for a given session id."""
|
||||
detail = memory_store.get_session_detail(session_id)
|
||||
if not detail:
|
||||
return jsonify({"error": "No detailed summary for this session."})
|
||||
return jsonify(detail)
|
||||
|
||||
@app.route("/api/search")
|
||||
def api_search():
|
||||
"""Unified memory search — facts + chunks + session one-liners."""
|
||||
q = (request.args.get("q") or "").strip()
|
||||
if not q:
|
||||
return jsonify([])
|
||||
|
||||
user = memory_store.get_or_create_user(memory_store.get_current_username())
|
||||
uid = user["id"]
|
||||
results = []
|
||||
|
||||
# Facts
|
||||
try:
|
||||
facts = memory_store.search_facts(uid, q, limit=5)
|
||||
for f in facts:
|
||||
results.append({
|
||||
"type": "fact",
|
||||
"content": f"[{f.get('category','')}] {f.get('fact','')}",
|
||||
"date": (f.get("created_at") or "")[:10],
|
||||
"score": 3,
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Chunks
|
||||
try:
|
||||
chunks = memory_store.search_chunks(uid, q, limit=5)
|
||||
for c in chunks:
|
||||
results.append({
|
||||
"type": "chunk",
|
||||
"content": (c.get("content") or "")[:300],
|
||||
"date": (c.get("created_at") or "")[:10],
|
||||
"score": 2,
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Session one-liners (simple LIKE — no FTS needed)
|
||||
try:
|
||||
from bigmind.db import db as _db
|
||||
with _db() as conn:
|
||||
rows = conn.execute(
|
||||
"""SELECT id, one_liner, started_at FROM sessions
|
||||
WHERE user_id=? AND ended_at IS NOT NULL
|
||||
AND one_liner LIKE ?
|
||||
ORDER BY started_at DESC LIMIT 5""",
|
||||
(uid, f"%{q}%"),
|
||||
).fetchall()
|
||||
for r in rows:
|
||||
results.append({
|
||||
"type": "session",
|
||||
"content": r["one_liner"],
|
||||
"date": (r.get("started_at") or "")[:10],
|
||||
"score": 1,
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Sort by type score desc, deduplicate by content
|
||||
seen = set()
|
||||
final = []
|
||||
for r in sorted(results, key=lambda x: -x["score"]):
|
||||
key = r["content"][:80]
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
final.append(r)
|
||||
|
||||
return jsonify(final[:15])
|
||||
|
||||
return app
|
||||
|
||||
|
||||
# ── Daemon thread startup ─────────────────────────────────────────────────────
|
||||
|
||||
def _port_in_use(port: int) -> bool:
|
||||
"""Return True if something is already listening on 127.0.0.1:<port>."""
|
||||
import socket
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.settimeout(0.2)
|
||||
return s.connect_ex(("127.0.0.1", port)) == 0
|
||||
|
||||
|
||||
def start_web_server() -> str:
|
||||
"""Start the Flask profile server in a background daemon thread.
|
||||
|
||||
Safe to call multiple times (only starts once). If another BigMind
|
||||
instance is already serving the port this process skips Flask startup
|
||||
gracefully — the MCP tools still work, the profile page is served by
|
||||
the other instance (same DB, same data).
|
||||
|
||||
The fix for the multi-IDE port-conflict bug: we check the port *before*
|
||||
setting _server_started = True. Previously the flag was set immediately
|
||||
after t.start(), so a failed Flask bind (Address already in use) left
|
||||
_server_started = True with no Flask running — permanent lock-out.
|
||||
"""
|
||||
global _server_started
|
||||
if _server_started:
|
||||
return f"http://localhost:{_PORT}"
|
||||
|
||||
if _port_in_use(_PORT):
|
||||
# Another BigMind process already owns the port — skip Flask startup.
|
||||
# Don't set _server_started = True so if that process dies and this
|
||||
# one is restarted, it can try again cleanly.
|
||||
logger.info(
|
||||
"BigMind profile server already running at http://localhost:%d "
|
||||
"(another IDE instance). Skipping Flask startup.", _PORT
|
||||
)
|
||||
return f"http://localhost:{_PORT}"
|
||||
|
||||
app = _create_app()
|
||||
_started_event = threading.Event()
|
||||
_bind_failed = [] # non-empty if Flask couldn't bind
|
||||
|
||||
def _run():
|
||||
import logging as _log
|
||||
_log.getLogger("werkzeug").setLevel(_log.ERROR)
|
||||
try:
|
||||
_started_event.set() # signal that the thread is running
|
||||
app.run(host="127.0.0.1", port=_PORT, debug=False, use_reloader=False)
|
||||
except OSError as exc:
|
||||
_bind_failed.append(str(exc))
|
||||
logger.warning("BigMind web server failed to bind port %d: %s", _PORT, exc)
|
||||
|
||||
t = threading.Thread(target=_run, daemon=True, name="BigMindWebServer")
|
||||
t.start()
|
||||
_started_event.wait(timeout=2.0) # wait for thread to actually start
|
||||
|
||||
# Only mark as started if Flask didn't immediately report a bind error
|
||||
if not _bind_failed:
|
||||
_server_started = True
|
||||
logger.info("BigMind profile server started at http://localhost:%d", _PORT)
|
||||
if _AUTOOPEN:
|
||||
import webbrowser, time
|
||||
time.sleep(1.0)
|
||||
webbrowser.open(f"http://localhost:{_PORT}")
|
||||
else:
|
||||
logger.warning(
|
||||
"BigMind web server could not start (port %d in use). "
|
||||
"Profile page unavailable from this IDE instance.", _PORT
|
||||
)
|
||||
|
||||
return f"http://localhost:{_PORT}"
|
||||
|
||||
|
||||
def get_profile_url() -> str:
|
||||
return f"http://localhost:{_PORT}"
|
||||
Reference in New Issue
Block a user