diff --git a/.githooks/pre-commit b/.githooks/pre-commit index f8219672..e2c112a4 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,25 +1,29 @@ #!/usr/bin/env bash -# Pre-commit hook: format + test via tox. -# Blocks the commit if formatting, imports, or tests fail. -# Current baseline: ~18s wall-clock. Limit set to 30s for headroom. +# Pre-commit hook: auto-format, then test via tox. +# Blocks the commit if tests fail. Formatting is applied automatically. # # Auto-activated by `make install` via git core.hooksPath. set -e -MAX_SECONDS=30 +MAX_SECONDS=60 + +# Auto-format staged files so formatting never blocks a commit +echo "Auto-formatting with black + isort..." +tox -e format -- 2>/dev/null || tox -e format +git add -u echo "Running pre-commit gate via tox (${MAX_SECONDS}s limit)..." # macOS lacks GNU timeout; use perl as a portable fallback. if command -v timeout &>/dev/null; then - timeout "${MAX_SECONDS}" tox -e pre-commit + timeout "${MAX_SECONDS}" tox -e unit else - perl -e "alarm ${MAX_SECONDS}; exec @ARGV" -- tox -e pre-commit + perl -e "alarm ${MAX_SECONDS}; exec @ARGV" -- tox -e unit fi exit_code=$? -# Re-stage any files that black/isort reformatted +# Re-stage any files that were reformatted git add -u if [ "$exit_code" -eq 142 ] || [ "$exit_code" -eq 124 ]; then diff --git a/src/config.py b/src/config.py index 30873701..35c9f637 100644 --- a/src/config.py +++ b/src/config.py @@ -112,6 +112,17 @@ class Settings(BaseSettings): # Set CORS_ORIGINS as a comma-separated list, e.g. "http://localhost:3000,https://example.com" cors_origins: list[str] = ["*"] + # Trusted hosts for the Host header check (TrustedHostMiddleware). + # Set TRUSTED_HOSTS as a comma-separated list. Wildcards supported (e.g. "*.ts.net"). + # Defaults include localhost + Tailscale MagicDNS. Add your Tailscale IP if needed. + trusted_hosts: list[str] = [ + "localhost", + "127.0.0.1", + "*.local", + "*.ts.net", + "testserver", + ] + # Environment mode: development | production # In production, security settings are strictly enforced. timmy_env: Literal["development", "production"] = "development" diff --git a/src/dashboard/app.py b/src/dashboard/app.py index c9c205b2..42e01484 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -51,6 +51,24 @@ from dashboard.routes.work_orders import router as work_orders_router from infrastructure.router.api import router as cascade_router +class _ColorFormatter(logging.Formatter): + """ANSI color formatter — red is reserved for ERROR/CRITICAL only.""" + + RESET = "\033[0m" + COLORS = { + logging.DEBUG: "\033[37m", # white/gray + logging.INFO: "\033[32m", # green + logging.WARNING: "\033[33m", # yellow + logging.ERROR: "\033[31m", # red + logging.CRITICAL: "\033[1;31m", # bold red + } + + def format(self, record: logging.LogRecord) -> str: + color = self.COLORS.get(record.levelno, self.RESET) + formatted = super().format(record) + return f"{color}{formatted}{self.RESET}" + + def _configure_logging() -> None: """Configure logging with console and optional rotating file handler.""" root_logger = logging.getLogger() @@ -59,13 +77,20 @@ def _configure_logging() -> None: console = logging.StreamHandler() console.setLevel(logging.INFO) console.setFormatter( - logging.Formatter( + _ColorFormatter( "%(asctime)s %(levelname)-8s %(name)s — %(message)s", datefmt="%H:%M:%S", ) ) root_logger.addHandler(console) + # Override uvicorn's default colored formatter so all console output + # uses our color scheme (red = ERROR/CRITICAL only). + for name in ("uvicorn", "uvicorn.error", "uvicorn.access"): + uv_logger = logging.getLogger(name) + uv_logger.handlers.clear() + uv_logger.propagate = True + if settings.error_log_enabled: from logging.handlers import RotatingFileHandler @@ -175,6 +200,13 @@ async def _discord_token_watcher() -> None: """Poll for DISCORD_TOKEN appearing in env or .env and auto-start Discord bot.""" from integrations.chat_bridge.vendors.discord import discord_bot + # Don't poll if discord.py isn't even installed + try: + import discord as _discord_check # noqa: F401 + except ImportError: + logger.debug("discord.py not installed — token watcher exiting") + return + while True: await asyncio.sleep(30) @@ -325,9 +357,11 @@ app.add_middleware(SecurityHeadersMiddleware, production=not settings.debug) app.add_middleware(CSRFMiddleware) # 4. Standard FastAPI middleware +# In development, allow all hosts (Tailscale IPs, MagicDNS, etc.) +_trusted = settings.trusted_hosts if settings.timmy_env == "production" else ["*"] app.add_middleware( TrustedHostMiddleware, - allowed_hosts=["localhost", "127.0.0.1", "*.local", "testserver"], + allowed_hosts=_trusted, ) app.add_middleware( diff --git a/src/integrations/chat_bridge/vendors/discord.py b/src/integrations/chat_bridge/vendors/discord.py index 43800e5e..746e0515 100644 --- a/src/integrations/chat_bridge/vendors/discord.py +++ b/src/integrations/chat_bridge/vendors/discord.py @@ -138,7 +138,9 @@ class DiscordVendor(ChatPlatform): try: import discord except ImportError: - logger.error("discord.py is not installed. " 'Run: pip install ".[discord]"') + logger.warning( + 'discord.py is not installed — skipping. Install with: pip install ".[discord]"' + ) return False try: diff --git a/src/timmy/tools.py b/src/timmy/tools.py index b6f36dd3..b7eca2cb 100644 --- a/src/timmy/tools.py +++ b/src/timmy/tools.py @@ -427,7 +427,7 @@ def create_full_toolkit(base_dir: str | Path | None = None): search_tools = DuckDuckGoTools() toolkit.register(search_tools.web_search, name="web_search") else: - logger.info("DuckDuckGo tools unavailable (ddgs not installed) — skipping web_search") + logger.debug("DuckDuckGo tools unavailable (ddgs not installed) — skipping web_search") # Python execution python_tools = PythonTools() diff --git a/tox.ini b/tox.ini index 4fffd6c9..531db405 100644 --- a/tox.ini +++ b/tox.ini @@ -166,7 +166,8 @@ commands = [testenv:dev] description = Start dashboard with auto-reload (local development) commands = - uvicorn dashboard.app:app --reload --host 0.0.0.0 --port 8000 + uvicorn dashboard.app:app --reload --host 0.0.0.0 --port 8000 \ + --reload-exclude ".claude" # ── All Tests (parallel) ─────────────────────────────────────────────────────