- Doc site UI improvements and fixes - Added Discord widget - Fixed some styling issues - Added command docs briefs and details - Briefs are shown in the right-hand panel - Details can be shown by clicking `Open full details` - Added ShaiWatchers' logo as the site favicon - Moved HTML template to static file for responsiveness improvements
149 lines
5.0 KiB
Python
149 lines
5.0 KiB
Python
# offline_preview.py
|
|
"""
|
|
ShaiWatcher offline preview (Discord-less)
|
|
|
|
Run from anywhere:
|
|
# optional (forces root if your layout is odd)
|
|
# export SHAI_PROJECT_ROOT=/absolute/path/to/shaiwatcher
|
|
# export SHAI_DOCS_HOST=127.0.0.1
|
|
# export SHAI_DOCS_PORT=8910
|
|
# export SHAI_OFFLINE=1
|
|
python3 offline_preview.py
|
|
"""
|
|
import os
|
|
import sys
|
|
import asyncio
|
|
import pathlib
|
|
import traceback
|
|
|
|
VERSION = "offline-preview-3"
|
|
|
|
# ---------- repo root discovery ----------
|
|
def _find_project_root() -> pathlib.Path:
|
|
cand = []
|
|
env = os.environ.get("SHAI_PROJECT_ROOT")
|
|
if env:
|
|
cand.append(pathlib.Path(env).resolve())
|
|
here = pathlib.Path(__file__).resolve().parent
|
|
cand.extend([
|
|
pathlib.Path.cwd().resolve(), # current working dir
|
|
here, # folder containing this file
|
|
here.parent, # one level up
|
|
here.parent.parent, # two levels up
|
|
])
|
|
# Also walk upwards from CWD a few levels to be forgiving
|
|
cur = pathlib.Path.cwd().resolve()
|
|
for _ in range(5):
|
|
cand.append(cur)
|
|
cur = cur.parent
|
|
|
|
tried = []
|
|
for c in cand:
|
|
tried.append(str(c))
|
|
if (c / "modules").is_dir() and (c / "modules" / "common").is_dir():
|
|
return c
|
|
|
|
raise FileNotFoundError(
|
|
"Could not locate project root with a 'modules/common' folder.\n"
|
|
f"Tried:\n - " + "\n - ".join(tried) +
|
|
"\nTip: set SHAI_PROJECT_ROOT=/absolute/path/to/repo"
|
|
)
|
|
|
|
PROJECT_ROOT = _find_project_root()
|
|
if str(PROJECT_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
# ---------- now safe to import project modules ----------
|
|
import discord # type: ignore
|
|
from discord.ext import commands # type: ignore
|
|
|
|
# Optional: your config helper if cogs expect it to exist
|
|
try:
|
|
from modules.common.settings import cfg as cfg_helper # noqa: F401
|
|
except Exception as e:
|
|
print("[OFFLINE] Warning: couldn't import cfg helper:", repr(e))
|
|
|
|
def _discover_extensions(project_root: pathlib.Path):
|
|
modules_path = project_root / "modules"
|
|
exts = []
|
|
for folder in modules_path.iterdir():
|
|
if not folder.is_dir():
|
|
continue
|
|
if folder.name == "common": # match your prod loader
|
|
continue
|
|
for file in folder.glob("*.py"):
|
|
if file.name == "__init__.py":
|
|
continue
|
|
exts.append(f"modules.{folder.name}.{file.stem}")
|
|
return exts
|
|
|
|
async def main():
|
|
print(f"[OFFLINE] ShaiWatcher offline preview v{VERSION}")
|
|
print(f"[OFFLINE] Project root -> {PROJECT_ROOT}")
|
|
|
|
# Keep intents minimal; we never connect anyway
|
|
intents = discord.Intents.none()
|
|
intents.guilds = True
|
|
|
|
bot = commands.Bot(command_prefix="!", intents=intents)
|
|
|
|
# Mark environment as offline for any cogs that check it
|
|
os.environ.setdefault("SHAI_OFFLINE", "1")
|
|
# Bind docs to localhost by default while testing
|
|
os.environ.setdefault("SHAI_DOCS_HOST", "127.0.0.1")
|
|
os.environ.setdefault("SHAI_DOCS_PORT", "8910")
|
|
os.environ.setdefault("SHAI_DOCS_TITLE", "ShaiWatcher (Offline Preview)")
|
|
|
|
# Optional: isolate data file so we don't touch prod paths
|
|
data_file = os.environ.get("SHAI_DATA", str(PROJECT_ROOT / ".offline_data.json"))
|
|
try:
|
|
from data_manager import DataManager # if your project has this at root
|
|
os.makedirs(os.path.dirname(data_file) or ".", exist_ok=True)
|
|
if not os.path.exists(data_file):
|
|
with open(data_file, "w", encoding="utf-8") as f:
|
|
f.write("{}")
|
|
bot.data_manager = DataManager(data_file)
|
|
print(f"[OFFLINE] DATA_FILE -> {data_file}")
|
|
except Exception as e:
|
|
print("[OFFLINE] DataManager unavailable/failed:", repr(e))
|
|
|
|
os.environ.setdefault("SHAI_OFFLINE", "1") # before loading cogs
|
|
|
|
# Load extensions exactly like prod
|
|
failures = 0
|
|
for ext in _discover_extensions(PROJECT_ROOT):
|
|
try:
|
|
await bot.load_extension(ext)
|
|
print(f"[Modules] Loaded: {ext}")
|
|
except Exception as e:
|
|
failures += 1
|
|
print(f"[Modules] Failed to load {ext}: {e}")
|
|
traceback.print_exc()
|
|
|
|
if failures:
|
|
print(f"[OFFLINE] Loaded with {failures} module error(s). See logs above.")
|
|
|
|
docs = bot.get_cog("DocsSite")
|
|
if docs and hasattr(docs, "force_ready"):
|
|
docs.force_ready(True)
|
|
|
|
# Make is_ready() == True so DocsSite serves immediately
|
|
try:
|
|
# discord.py sets this in login/READY; we emulate it
|
|
if not hasattr(bot, "_ready") or bot._ready is None: # type: ignore[attr-defined]
|
|
bot._ready = asyncio.Event() # type: ignore[attr-defined]
|
|
bot._ready.set() # type: ignore[attr-defined]
|
|
except Exception:
|
|
pass
|
|
|
|
print("[OFFLINE] Docs: http://%s:%s/"
|
|
% (os.environ.get("SHAI_DOCS_HOST", "0.0.0.0"),
|
|
os.environ.get("SHAI_DOCS_PORT", "8911")))
|
|
print("[OFFLINE] This runner does NOT connect to Discord.")
|
|
|
|
# Idle forever; DocsSite runs in its own daemon thread
|
|
await asyncio.Event().wait()
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|