forked from Rockachopa/Timmy-time-dashboard
Audit cleanup: security fixes, code reduction, test hygiene (#131)
This commit is contained in:
committed by
GitHub
parent
e8f1dea3ec
commit
aff3edb06a
@@ -5,7 +5,6 @@ No OpenAI dependency. Runs 100% locally on CPU.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Union
|
||||
|
||||
|
||||
@@ -77,9 +77,9 @@ class DistributedWorker:
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
except:
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
pass
|
||||
|
||||
|
||||
# Check for ROCm
|
||||
if os.path.exists("/opt/rocm"):
|
||||
return True
|
||||
@@ -93,11 +93,11 @@ class DistributedWorker:
|
||||
)
|
||||
if "Metal" in result.stdout:
|
||||
return True
|
||||
except:
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
pass
|
||||
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _has_internet(self) -> bool:
|
||||
"""Check if we have internet connectivity."""
|
||||
try:
|
||||
@@ -106,9 +106,9 @@ class DistributedWorker:
|
||||
capture_output=True, timeout=5
|
||||
)
|
||||
return result.returncode == 0
|
||||
except:
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return False
|
||||
|
||||
|
||||
def _get_memory_gb(self) -> float:
|
||||
"""Get total system memory in GB."""
|
||||
try:
|
||||
@@ -125,7 +125,7 @@ class DistributedWorker:
|
||||
if line.startswith("MemTotal:"):
|
||||
kb = int(line.split()[1])
|
||||
return kb / (1024**2)
|
||||
except:
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return 8.0 # Assume 8GB if we can't detect
|
||||
|
||||
@@ -136,9 +136,9 @@ class DistributedWorker:
|
||||
["which", cmd], capture_output=True, timeout=5
|
||||
)
|
||||
return result.returncode == 0
|
||||
except:
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return False
|
||||
|
||||
|
||||
def _register_default_handlers(self):
|
||||
"""Register built-in task handlers."""
|
||||
self._handlers = {
|
||||
|
||||
@@ -215,6 +215,7 @@ OLLAMA_MODEL_FALLBACK: str = "qwen2.5:14b"
|
||||
def check_ollama_model_available(model_name: str) -> bool:
|
||||
"""Check if a specific Ollama model is available locally."""
|
||||
try:
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
url = settings.ollama_url.replace("localhost", "127.0.0.1")
|
||||
@@ -224,12 +225,12 @@ def check_ollama_model_available(model_name: str) -> bool:
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=5) as response:
|
||||
import json
|
||||
|
||||
data = json.loads(response.read().decode())
|
||||
models = [m.get("name", "").split(":")[0] for m in data.get("models", [])]
|
||||
# Check for exact match or model name without tag
|
||||
return any(model_name in m or m in model_name for m in models)
|
||||
models = [m.get("name", "") for m in data.get("models", [])]
|
||||
return any(
|
||||
model_name == m or model_name == m.split(":")[0] or m.startswith(model_name)
|
||||
for m in models
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from config import settings
|
||||
from dashboard.routes.agents import router as agents_router
|
||||
from dashboard.routes.health import router as health_router
|
||||
@@ -282,8 +280,8 @@ static_dir = PROJECT_ROOT / "static"
|
||||
if static_dir.exists():
|
||||
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
||||
|
||||
# Global templates instance
|
||||
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
||||
# Shared templates instance
|
||||
from dashboard.templating import templates # noqa: E402
|
||||
|
||||
|
||||
# Include routers
|
||||
|
||||
@@ -187,7 +187,7 @@ class CSRFMiddleware(BaseHTTPMiddleware):
|
||||
"/lightning/webhook",
|
||||
"/_internal/",
|
||||
]
|
||||
return any(pattern in path for pattern in exempt_patterns)
|
||||
return any(path.startswith(pattern) for pattern in exempt_patterns)
|
||||
|
||||
async def _validate_request(self, request: Request, csrf_cookie: Optional[str]) -> bool:
|
||||
"""Validate the CSRF token in the request.
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from timmy.session import chat as timmy_chat
|
||||
from dashboard.store import message_log
|
||||
from dashboard.templating import templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/agents", tags=["agents"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("")
|
||||
|
||||
@@ -7,19 +7,17 @@ POST /briefing/approvals/{id}/reject — reject an item (HTMX)
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from timmy.briefing import engine as briefing_engine
|
||||
from timmy import approvals as approval_store
|
||||
from dashboard.templating import templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/briefing", tags=["briefing"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("", response_class=HTMLResponse)
|
||||
|
||||
@@ -5,21 +5,15 @@ from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dashboard.models.calm import JournalEntry, Task, TaskCertainty, TaskState
|
||||
from dashboard.models.database import SessionLocal, engine, get_db
|
||||
|
||||
# Create database tables (if not already created by Alembic)
|
||||
# This is typically handled by Alembic migrations in a production environment
|
||||
# from dashboard.models.database import Base
|
||||
# Base.metadata.create_all(bind=engine)
|
||||
from dashboard.templating import templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(tags=["calm"])
|
||||
templates = Jinja2Templates(directory="src/dashboard/templates")
|
||||
|
||||
|
||||
# Helper functions for state machine logic
|
||||
|
||||
@@ -15,7 +15,7 @@ import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, File, Request, UploadFile
|
||||
from fastapi import APIRouter, File, HTTPException, Request, UploadFile
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from config import settings
|
||||
@@ -27,6 +27,7 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api", tags=["chat-api"])
|
||||
|
||||
_UPLOAD_DIR = os.path.join("data", "chat-uploads")
|
||||
_MAX_UPLOAD_SIZE = 50 * 1024 * 1024 # 50 MB
|
||||
|
||||
|
||||
# ── POST /api/chat ────────────────────────────────────────────────────────────
|
||||
@@ -112,11 +113,13 @@ async def api_upload(file: UploadFile = File(...)):
|
||||
os.makedirs(_UPLOAD_DIR, exist_ok=True)
|
||||
|
||||
suffix = uuid.uuid4().hex[:12]
|
||||
safe_name = (file.filename or "upload").replace("/", "_").replace("\\", "_")
|
||||
safe_name = os.path.basename(file.filename or "upload")
|
||||
stored_name = f"{suffix}-{safe_name}"
|
||||
file_path = os.path.join(_UPLOAD_DIR, stored_name)
|
||||
|
||||
contents = await file.read()
|
||||
if len(contents) > _MAX_UPLOAD_SIZE:
|
||||
raise HTTPException(status_code=413, detail="File too large (max 50 MB)")
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(contents)
|
||||
|
||||
|
||||
@@ -9,18 +9,16 @@ GET /grok/stats — Usage statistics (JSON)
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from config import settings
|
||||
from dashboard.templating import templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/grok", tags=["grok"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
# In-memory toggle state (persists per process lifetime)
|
||||
_grok_mode_active: bool = False
|
||||
|
||||
@@ -4,16 +4,13 @@ DEPRECATED: Personas replaced by brain task queue.
|
||||
This module is kept for UI compatibility.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from brain.client import BrainClient
|
||||
from dashboard.templating import templates
|
||||
|
||||
router = APIRouter(tags=["marketplace"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
# Orchestrator only — personas deprecated
|
||||
AGENT_CATALOG = [
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
"""Memory (vector store) routes for browsing and searching memories."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Form, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from timmy.memory.vector_store import (
|
||||
store_memory,
|
||||
@@ -17,9 +15,9 @@ from timmy.memory.vector_store import (
|
||||
update_personal_fact,
|
||||
delete_memory,
|
||||
)
|
||||
from dashboard.templating import templates
|
||||
|
||||
router = APIRouter(prefix="/memory", tags=["memory"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("", response_class=HTMLResponse)
|
||||
|
||||
@@ -8,16 +8,13 @@ The /mobile/local endpoint loads a small LLM directly into the
|
||||
browser via WebLLM so Timmy can run on an iPhone with no server.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from config import settings
|
||||
from dashboard.templating import templates
|
||||
|
||||
router = APIRouter(tags=["mobile"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("/mobile", response_class=HTMLResponse)
|
||||
|
||||
@@ -10,7 +10,6 @@ from typing import Any, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pydantic import BaseModel
|
||||
|
||||
from config import settings
|
||||
@@ -21,12 +20,12 @@ from infrastructure.models.registry import (
|
||||
ModelRole,
|
||||
model_registry,
|
||||
)
|
||||
from dashboard.templating import templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/models", tags=["models"])
|
||||
api_router = APIRouter(prefix="/api/v1/models", tags=["models-api"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
# ── Pydantic schemas ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
"""Cascade Router status routes."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from timmy.cascade_adapter import get_cascade_adapter
|
||||
from dashboard.templating import templates
|
||||
|
||||
router = APIRouter(prefix="/router", tags=["router"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("/status", response_class=HTMLResponse)
|
||||
|
||||
@@ -9,18 +9,16 @@ GET /spark/predictions — HTMX partial: EIDOS predictions
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from spark.engine import spark_engine
|
||||
from dashboard.templating import templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/spark", tags=["spark"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("/ui", response_class=HTMLResponse)
|
||||
|
||||
@@ -2,19 +2,17 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from spark.engine import spark_engine
|
||||
from dashboard.templating import templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/swarm", tags=["swarm"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("/events", response_class=HTMLResponse)
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
"""System-level dashboard routes (ledger, upgrades, etc.)."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from dashboard.templating import templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(tags=["system"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("/lightning/ledger", response_class=HTMLResponse)
|
||||
|
||||
@@ -8,16 +8,15 @@ POST /celery/api/{id}/revoke — cancel a running task
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from dashboard.templating import templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/celery", tags=["celery"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
# In-memory record of submitted task IDs for the dashboard display.
|
||||
# In production this would use the Celery result backend directly,
|
||||
|
||||
@@ -6,18 +6,16 @@ GET /thinking/api/{id}/chain — follow a thought chain
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from timmy.thinking import thinking_engine
|
||||
from dashboard.templating import templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/thinking", tags=["thinking"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("", response_class=HTMLResponse)
|
||||
|
||||
@@ -3,16 +3,13 @@
|
||||
Shows available tools and usage statistics.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from timmy.tools import get_all_available_tools
|
||||
from dashboard.templating import templates
|
||||
|
||||
router = APIRouter(tags=["tools"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("/tools", response_class=HTMLResponse)
|
||||
|
||||
@@ -9,16 +9,14 @@ import logging
|
||||
|
||||
from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pathlib import Path
|
||||
|
||||
from integrations.voice.nlu import detect_intent, extract_command
|
||||
from timmy.agent import create_timmy
|
||||
from dashboard.templating import templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/voice", tags=["voice"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.post("/nlu")
|
||||
|
||||
7
src/dashboard/templating.py
Normal file
7
src/dashboard/templating.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Shared Jinja2Templates instance for all dashboard routes."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates"))
|
||||
@@ -18,8 +18,6 @@ from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
|
||||
@@ -7,6 +7,7 @@ system events.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import asdict, dataclass
|
||||
@@ -34,8 +35,7 @@ class WebSocketManager:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._connections: list[WebSocket] = []
|
||||
self._event_history: list[WSEvent] = []
|
||||
self._max_history = 100
|
||||
self._event_history: collections.deque[WSEvent] = collections.deque(maxlen=100)
|
||||
|
||||
async def connect(self, websocket: WebSocket) -> None:
|
||||
"""Accept a new WebSocket connection."""
|
||||
@@ -46,7 +46,7 @@ class WebSocketManager:
|
||||
len(self._connections),
|
||||
)
|
||||
# Send recent history to the new client
|
||||
for event in self._event_history[-20:]:
|
||||
for event in list(self._event_history)[-20:]:
|
||||
try:
|
||||
await websocket.send_text(event.to_json())
|
||||
except Exception:
|
||||
@@ -69,8 +69,6 @@ class WebSocketManager:
|
||||
timestamp=datetime.now(timezone.utc).isoformat(),
|
||||
)
|
||||
self._event_history.append(ws_event)
|
||||
if len(self._event_history) > self._max_history:
|
||||
self._event_history = self._event_history[-self._max_history:]
|
||||
|
||||
message = ws_event.to_json()
|
||||
disconnected = []
|
||||
@@ -78,7 +76,10 @@ class WebSocketManager:
|
||||
for ws in self._connections:
|
||||
try:
|
||||
await ws.send_text(message)
|
||||
except ConnectionError:
|
||||
disconnected.append(ws)
|
||||
except Exception:
|
||||
logger.warning("Unexpected WebSocket send error", exc_info=True)
|
||||
disconnected.append(ws)
|
||||
|
||||
# Clean up dead connections
|
||||
@@ -128,8 +129,6 @@ class WebSocketManager:
|
||||
Returns:
|
||||
Number of clients notified
|
||||
"""
|
||||
import json
|
||||
|
||||
message = json.dumps(data)
|
||||
disconnected = []
|
||||
count = 0
|
||||
|
||||
@@ -20,7 +20,7 @@ from agno.agent import Agent
|
||||
from agno.db.sqlite import SqliteDb
|
||||
from agno.models.ollama import Ollama
|
||||
|
||||
from config import settings
|
||||
from config import check_ollama_model_available, settings
|
||||
from timmy.prompts import get_system_prompt
|
||||
from timmy.tools import create_full_toolkit
|
||||
|
||||
@@ -64,27 +64,7 @@ _SMALL_MODEL_PATTERNS = (
|
||||
|
||||
def _check_model_available(model_name: str) -> bool:
|
||||
"""Check if an Ollama model is available locally."""
|
||||
try:
|
||||
import urllib.request
|
||||
import json
|
||||
|
||||
url = settings.ollama_url.replace("localhost", "127.0.0.1")
|
||||
req = urllib.request.Request(
|
||||
f"{url}/api/tags",
|
||||
method="GET",
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=5) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
models = [m.get("name", "") for m in data.get("models", [])]
|
||||
# Check for exact match or model name without tag
|
||||
return any(
|
||||
model_name == m or model_name == m.split(":")[0] or m.startswith(model_name)
|
||||
for m in models
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.debug("Could not check model availability: %s", exc)
|
||||
return False
|
||||
return check_ollama_model_available(model_name)
|
||||
|
||||
|
||||
def _pull_model(model_name: str) -> bool:
|
||||
|
||||
@@ -121,67 +121,5 @@ def down():
|
||||
subprocess.run(["docker", "compose", "down"], check=True)
|
||||
|
||||
|
||||
@app.command(name="ingest-report")
|
||||
def ingest_report(
|
||||
file: Optional[str] = typer.Argument(
|
||||
None, help="Path to JSON report file (reads stdin if omitted)",
|
||||
),
|
||||
dry_run: bool = typer.Option(
|
||||
False, "--dry-run", help="Validate report and show what would be created",
|
||||
),
|
||||
):
|
||||
"""Ingest a structured test report and create bug_report tasks.
|
||||
|
||||
Reads a JSON report with an array of bugs and creates one task per bug
|
||||
in the internal task queue. The task processor will then attempt to
|
||||
create GitHub Issues for each.
|
||||
|
||||
Examples:
|
||||
timmy ingest-report report.json
|
||||
timmy ingest-report --dry-run report.json
|
||||
cat report.json | timmy ingest-report
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Read input
|
||||
if file:
|
||||
try:
|
||||
with open(file) as f:
|
||||
raw = f.read()
|
||||
except FileNotFoundError:
|
||||
typer.echo(f"File not found: {file}", err=True)
|
||||
raise typer.Exit(1)
|
||||
else:
|
||||
if sys.stdin.isatty():
|
||||
typer.echo("Reading from stdin (paste JSON, then Ctrl+D)...")
|
||||
raw = sys.stdin.read()
|
||||
|
||||
# Parse JSON
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError as exc:
|
||||
typer.echo(f"Invalid JSON: {exc}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
reporter = data.get("reporter", "unknown")
|
||||
bugs = data.get("bugs", [])
|
||||
|
||||
if not bugs:
|
||||
typer.echo("No bugs in report.", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
typer.echo(f"Report: {len(bugs)} bug(s) from {reporter}")
|
||||
|
||||
if dry_run:
|
||||
for bug in bugs:
|
||||
typer.echo(f" [{bug.get('severity', '?')}] {bug.get('title', '(no title)')}")
|
||||
typer.echo("(dry run — no tasks created)")
|
||||
return
|
||||
|
||||
typer.echo("Task queue not available (swarm module removed).", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
app()
|
||||
|
||||
Reference in New Issue
Block a user