From d2c80fbf4c297abf0a9b5e9fc3e129fc01325821 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Feb 2026 21:30:39 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20Phase=202a=20=E2=80=94=20consolidat?= =?UTF-8?q?e=20dashboard=20routes=20(27=E2=86=9222=20files)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge related route files to reduce sprawl: - voice.py ← voice_enhanced.py (enhanced pipeline merged in) - swarm.py ← swarm_internal.py + swarm_ws.py (internal API + WebSocket) - self_coding.py ← self_modify.py (self-modify endpoints merged in) - Delete mobile_test.py route + template (test-only page, not for prod) - Delete test_xss_prevention.py (tested the deleted mobile_test page) Update app.py to use consolidated imports. Update test_voice_enhanced.py patch paths. Remove mobile_test.py from coverage omit (file deleted). 27 route files → 22. Tests: 1502 passed (1 removed with deleted page). https://claude.ai/code/session_019oMFNvD8uSGSSmBMGkBfQN --- pyproject.toml | 1 - src/dashboard/app.py | 16 +- src/dashboard/routes/mobile_test.py | 257 ------------- src/dashboard/routes/self_coding.py | 62 +++- src/dashboard/routes/self_modify.py | 71 ---- src/dashboard/routes/swarm.py | 101 +++++- src/dashboard/routes/swarm_internal.py | 115 ------ src/dashboard/routes/swarm_ws.py | 33 -- src/dashboard/routes/voice.py | 111 +++++- src/dashboard/routes/voice_enhanced.py | 116 ------ src/dashboard/templates/mobile_test.html | 422 ---------------------- tests/integrations/test_voice_enhanced.py | 6 +- tests/security/test_xss_prevention.py | 25 -- 13 files changed, 275 insertions(+), 1061 deletions(-) delete mode 100644 src/dashboard/routes/mobile_test.py delete mode 100644 src/dashboard/routes/self_modify.py delete mode 100644 src/dashboard/routes/swarm_internal.py delete mode 100644 src/dashboard/routes/swarm_ws.py delete mode 100644 src/dashboard/routes/voice_enhanced.py delete mode 100644 src/dashboard/templates/mobile_test.html delete mode 100644 tests/security/test_xss_prevention.py diff --git a/pyproject.toml b/pyproject.toml index 3112a6c6..f8810629 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,7 +130,6 @@ markers = [ source = ["src"] omit = [ "*/tests/*", - "src/dashboard/routes/mobile_test.py", ] [tool.coverage.report] diff --git a/src/dashboard/app.py b/src/dashboard/app.py index e4dcc899..3ea65570 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -12,21 +12,17 @@ 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 -from dashboard.routes.mobile_test import router as mobile_test_router from dashboard.routes.swarm import router as swarm_router +from dashboard.routes.swarm import internal_router as swarm_internal_router from dashboard.routes.marketplace import router as marketplace_router from dashboard.routes.voice import router as voice_router -from dashboard.routes.voice_enhanced import router as voice_enhanced_router from dashboard.routes.mobile import router as mobile_router -from dashboard.routes.swarm_ws import router as swarm_ws_router from dashboard.routes.briefing import router as briefing_router from dashboard.routes.telegram import router as telegram_router -from dashboard.routes.swarm_internal import router as swarm_internal_router from dashboard.routes.tools import router as tools_router from dashboard.routes.spark import router as spark_router from dashboard.routes.creative import router as creative_router from dashboard.routes.discord import router as discord_router -from dashboard.routes.self_modify import router as self_modify_router from dashboard.routes.events import router as events_router from dashboard.routes.ledger import router as ledger_router from dashboard.routes.memory import router as memory_router @@ -36,6 +32,7 @@ from dashboard.routes.work_orders import router as work_orders_router from dashboard.routes.tasks import router as tasks_router from dashboard.routes.scripture import router as scripture_router from dashboard.routes.self_coding import router as self_coding_router +from dashboard.routes.self_coding import self_modify_router from dashboard.routes.hands import router as hands_router from router.api import router as cascade_router @@ -131,7 +128,7 @@ async def lifespan(app: FastAPI): logger.info("MCP auto-bootstrap: %d tools registered", len(registered)) except Exception as exc: logger.warning("MCP auto-bootstrap failed: %s", exc) - + # Initialise Spark Intelligence engine from spark.engine import spark_engine if spark_engine.enabled: @@ -178,20 +175,18 @@ app.mount("/static", StaticFiles(directory=str(PROJECT_ROOT / "static")), name=" app.include_router(health_router) app.include_router(agents_router) -app.include_router(mobile_test_router) app.include_router(swarm_router) +app.include_router(swarm_internal_router) app.include_router(marketplace_router) app.include_router(voice_router) -app.include_router(voice_enhanced_router) app.include_router(mobile_router) -app.include_router(swarm_ws_router) app.include_router(briefing_router) app.include_router(telegram_router) -app.include_router(swarm_internal_router) app.include_router(tools_router) app.include_router(spark_router) app.include_router(creative_router) app.include_router(discord_router) +app.include_router(self_coding_router) app.include_router(self_modify_router) app.include_router(events_router) app.include_router(ledger_router) @@ -201,7 +196,6 @@ app.include_router(upgrades_router) app.include_router(work_orders_router) app.include_router(tasks_router) app.include_router(scripture_router) -app.include_router(self_coding_router) app.include_router(hands_router) app.include_router(cascade_router) diff --git a/src/dashboard/routes/mobile_test.py b/src/dashboard/routes/mobile_test.py deleted file mode 100644 index ef22337d..00000000 --- a/src/dashboard/routes/mobile_test.py +++ /dev/null @@ -1,257 +0,0 @@ -"""Mobile HITL (Human-in-the-Loop) test checklist route. - -GET /mobile-test — interactive checklist for a human tester on their phone. - -Each scenario specifies what to do and what to observe. The tester marks -each one PASS / FAIL / SKIP. Results are stored in sessionStorage so they -survive page scrolling without hitting the server. -""" - -from pathlib import Path - -from fastapi import APIRouter, Request -from fastapi.responses import HTMLResponse -from fastapi.templating import Jinja2Templates - -router = APIRouter(tags=["mobile-test"]) -templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates")) - -# ── Test scenarios ──────────────────────────────────────────────────────────── -# Each dict: id, category, title, steps (list), expected -SCENARIOS = [ - # Layout - { - "id": "L01", - "category": "Layout", - "title": "Sidebar renders as horizontal strip", - "steps": [ - "Open the Mission Control page on your phone.", - "Look at the top section above the chat window.", - ], - "expected": ( - "AGENTS and SYSTEM HEALTH panels appear side-by-side in a " - "horizontally scrollable strip — not stacked vertically." - ), - }, - { - "id": "L02", - "category": "Layout", - "title": "Sidebar panels are horizontally scrollable", - "steps": [ - "Swipe left/right on the AGENTS / SYSTEM HEALTH strip.", - ], - "expected": "Both panels slide smoothly; no page scroll is triggered.", - }, - { - "id": "L03", - "category": "Layout", - "title": "Chat panel fills ≥ 60 % of viewport height", - "steps": [ - "Look at the TIMMY INTERFACE chat card below the strip.", - ], - "expected": "The chat card occupies at least 60 % of the visible screen height.", - }, - { - "id": "L04", - "category": "Layout", - "title": "Header stays fixed while chat scrolls", - "steps": [ - "Send several messages until the chat overflows.", - "Scroll the chat log up and down.", - ], - "expected": "The TIMMY TIME / MISSION CONTROL header remains pinned at the top.", - }, - { - "id": "L05", - "category": "Layout", - "title": "No horizontal page overflow", - "steps": [ - "Try swiping left or right anywhere on the page.", - ], - "expected": "The page does not scroll horizontally; nothing is cut off.", - }, - # Touch & Input - { - "id": "T01", - "category": "Touch & Input", - "title": "iOS does NOT zoom when tapping the input", - "steps": [ - "Tap the message input field once.", - "Watch whether the browser zooms in.", - ], - "expected": "The keyboard rises; the layout does NOT zoom in.", - }, - { - "id": "T02", - "category": "Touch & Input", - "title": "Keyboard return key is labelled 'Send'", - "steps": [ - "Tap the message input to open the iOS/Android keyboard.", - "Look at the return / action key in the bottom-right of the keyboard.", - ], - "expected": "The key is labelled 'Send' (not 'Return' or 'Go').", - }, - { - "id": "T03", - "category": "Touch & Input", - "title": "Send button is easy to tap (≥ 44 px tall)", - "steps": [ - "Try tapping the SEND button with your thumb.", - ], - "expected": "The button registers the tap reliably on the first attempt.", - }, - { - "id": "T04", - "category": "Touch & Input", - "title": "SEND button disabled during in-flight request", - "steps": [ - "Type a message and press SEND.", - "Immediately try to tap SEND again before a response arrives.", - ], - "expected": "The button is visually disabled; no duplicate message is sent.", - }, - { - "id": "T05", - "category": "Touch & Input", - "title": "Empty message cannot be submitted", - "steps": [ - "Leave the input blank.", - "Tap SEND.", - ], - "expected": "Nothing is submitted; the form shows a required-field indicator.", - }, - { - "id": "T06", - "category": "Touch & Input", - "title": "CLEAR button shows confirmation dialog", - "steps": [ - "Send at least one message.", - "Tap the CLEAR button in the top-right of the chat header.", - ], - "expected": "A browser confirmation dialog appears before history is cleared.", - }, - # Chat behaviour - { - "id": "C01", - "category": "Chat", - "title": "Chat auto-scrolls to the latest message", - "steps": [ - "Scroll the chat log to the top.", - "Send a new message.", - ], - "expected": "After the response arrives the chat automatically scrolls to the bottom.", - }, - { - "id": "C02", - "category": "Chat", - "title": "Multi-turn conversation — Timmy remembers context", - "steps": [ - "Send: 'My name is .'", - "Then send: 'What is my name?'", - ], - "expected": "Timmy replies with your name, demonstrating conversation memory.", - }, - { - "id": "C03", - "category": "Chat", - "title": "Loading indicator appears while waiting", - "steps": [ - "Send a message and watch the SEND button.", - ], - "expected": "A blinking cursor (▋) appears next to SEND while the response is loading.", - }, - { - "id": "C04", - "category": "Chat", - "title": "Offline error is shown gracefully", - "steps": [ - "Stop Ollama on your host machine (or disconnect from Wi-Fi temporarily).", - "Send a message from your phone.", - ], - "expected": "A red 'Timmy is offline' error appears in the chat — no crash or spinner hang.", - }, - # Health panel - { - "id": "H01", - "category": "Health", - "title": "Health panel shows Ollama UP when running", - "steps": [ - "Ensure Ollama is running on your host.", - "Check the SYSTEM HEALTH panel.", - ], - "expected": "OLLAMA badge shows green UP.", - }, - { - "id": "H02", - "category": "Health", - "title": "Health panel auto-refreshes without reload", - "steps": [ - "Start Ollama if it is not running.", - "Wait up to 35 seconds with the page open.", - ], - "expected": "The OLLAMA badge flips from DOWN → UP automatically, without a page reload.", - }, - # Scroll & overscroll - { - "id": "S01", - "category": "Scroll", - "title": "No rubber-band / bounce on the main page", - "steps": [ - "Scroll to the very top of the page.", - "Continue pulling downward.", - ], - "expected": "The page does not bounce or show a white gap — overscroll is suppressed.", - }, - { - "id": "S02", - "category": "Scroll", - "title": "Chat log scrolls independently inside the card", - "steps": [ - "Scroll inside the chat log area.", - ], - "expected": "The chat log scrolls smoothly; the outer page does not move.", - }, - # Safe area / notch - { - "id": "N01", - "category": "Notch / Home Bar", - "title": "Header clears the status bar / Dynamic Island", - "steps": [ - "On a notched iPhone (Face ID), look at the top of the page.", - ], - "expected": "The TIMMY TIME header text is not obscured by the notch or Dynamic Island.", - }, - { - "id": "N02", - "category": "Notch / Home Bar", - "title": "Chat input not hidden behind home indicator", - "steps": [ - "Tap the input field and look at the bottom of the screen.", - ], - "expected": "The input row sits above the iPhone home indicator bar — nothing is cut off.", - }, - # Clock - { - "id": "X01", - "category": "Live UI", - "title": "Clock updates every second", - "steps": [ - "Look at the time display in the top-right of the header.", - "Watch for 3 seconds.", - ], - "expected": "The time increments each second in HH:MM:SS format.", - }, -] - - -@router.get("/mobile-test", response_class=HTMLResponse) -async def mobile_test(request: Request): - """Interactive HITL mobile test checklist — open on your phone.""" - categories: dict[str, list] = {} - for s in SCENARIOS: - categories.setdefault(s["category"], []).append(s) - return templates.TemplateResponse( - request, - "mobile_test.html", - {"scenarios": SCENARIOS, "categories": categories, "total": len(SCENARIOS)}, - ) diff --git a/src/dashboard/routes/self_coding.py b/src/dashboard/routes/self_coding.py index cf30f82e..53f9b8af 100644 --- a/src/dashboard/routes/self_coding.py +++ b/src/dashboard/routes/self_coding.py @@ -5,17 +5,21 @@ API endpoints and HTMX views for the self-coding system: - Stats dashboard - Manual task execution - Real-time status updates +- Self-modification loop (/self-modify/*) """ from __future__ import annotations +import asyncio import logging from typing import Optional -from fastapi import APIRouter, Form, Request +from fastapi import APIRouter, Form, HTTPException, Request from fastapi.responses import HTMLResponse, JSONResponse from pydantic import BaseModel +from config import settings + from self_coding import ( CodebaseIndexer, ModificationJournal, @@ -366,3 +370,59 @@ async def journal_entry_detail(request: Request, attempt_id: int): "entry": entry, }, ) + + +# ── Self-Modification Routes (/self-modify/*) ─────────────────────────── + +self_modify_router = APIRouter(prefix="/self-modify", tags=["self-modify"]) + + +@self_modify_router.post("/run") +async def run_self_modify( + instruction: str = Form(...), + target_files: str = Form(""), + dry_run: bool = Form(False), + speak_result: bool = Form(False), +): + """Execute a self-modification loop.""" + if not settings.self_modify_enabled: + raise HTTPException(403, "Self-modification is disabled") + + from self_modify.loop import SelfModifyLoop, ModifyRequest + + files = [f.strip() for f in target_files.split(",") if f.strip()] + request = ModifyRequest( + instruction=instruction, + target_files=files, + dry_run=dry_run, + ) + + loop = SelfModifyLoop() + result = await asyncio.to_thread(loop.run, request) + + if speak_result and result.success: + try: + from timmy_serve.voice_tts import voice_tts + if voice_tts.available: + voice_tts.speak( + f"Code modification complete. " + f"{len(result.files_changed)} files changed. Tests passing." + ) + except Exception: + pass + + return { + "success": result.success, + "files_changed": result.files_changed, + "test_passed": result.test_passed, + "commit_sha": result.commit_sha, + "branch_name": result.branch_name, + "error": result.error, + "attempts": result.attempts, + } + + +@self_modify_router.get("/status") +async def self_modify_status(): + """Return whether self-modification is enabled.""" + return {"enabled": settings.self_modify_enabled} diff --git a/src/dashboard/routes/self_modify.py b/src/dashboard/routes/self_modify.py deleted file mode 100644 index 2e0cf74a..00000000 --- a/src/dashboard/routes/self_modify.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Self-modification routes — /self-modify endpoints. - -Exposes the edit-test-commit loop as a REST API. Gated by -``SELF_MODIFY_ENABLED`` (default False). -""" - -import asyncio -import logging - -from fastapi import APIRouter, Form, HTTPException - -from config import settings - -logger = logging.getLogger(__name__) - -router = APIRouter(prefix="/self-modify", tags=["self-modify"]) - - -@router.post("/run") -async def run_self_modify( - instruction: str = Form(...), - target_files: str = Form(""), - dry_run: bool = Form(False), - speak_result: bool = Form(False), -): - """Execute a self-modification loop. - - Returns the ModifyResult as JSON. - """ - if not settings.self_modify_enabled: - raise HTTPException(403, "Self-modification is disabled") - - from self_modify.loop import SelfModifyLoop, ModifyRequest - - files = [f.strip() for f in target_files.split(",") if f.strip()] - request = ModifyRequest( - instruction=instruction, - target_files=files, - dry_run=dry_run, - ) - - loop = SelfModifyLoop() - result = await asyncio.to_thread(loop.run, request) - - if speak_result and result.success: - try: - from timmy_serve.voice_tts import voice_tts - - if voice_tts.available: - voice_tts.speak( - f"Code modification complete. " - f"{len(result.files_changed)} files changed. Tests passing." - ) - except Exception: - pass - - return { - "success": result.success, - "files_changed": result.files_changed, - "test_passed": result.test_passed, - "commit_sha": result.commit_sha, - "branch_name": result.branch_name, - "error": result.error, - "attempts": result.attempts, - } - - -@router.get("/status") -async def self_modify_status(): - """Return whether self-modification is enabled.""" - return {"enabled": settings.self_modify_enabled} diff --git a/src/dashboard/routes/swarm.py b/src/dashboard/routes/swarm.py index 263cac0d..f9aec8ab 100644 --- a/src/dashboard/routes/swarm.py +++ b/src/dashboard/routes/swarm.py @@ -1,22 +1,28 @@ -"""Swarm dashboard routes — /swarm/* endpoints. +"""Swarm dashboard routes — /swarm/*, /internal/*, and /swarm/live endpoints. Provides REST endpoints for managing the swarm: listing agents, -spawning sub-agents, posting tasks, and viewing auction results. +spawning sub-agents, posting tasks, viewing auction results, Docker +container agent HTTP API, and WebSocket live feed. """ import asyncio +import logging from datetime import datetime, timezone from pathlib import Path from typing import Optional -from fastapi import APIRouter, Form, HTTPException, Request +from fastapi import APIRouter, Form, HTTPException, Request, WebSocket, WebSocketDisconnect from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates +from pydantic import BaseModel from swarm import learner as swarm_learner from swarm import registry from swarm.coordinator import coordinator from swarm.tasks import TaskStatus, update_task +from ws_manager.handler import ws_manager + +logger = logging.getLogger(__name__) router = APIRouter(prefix="/swarm", tags=["swarm"]) templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates")) @@ -325,3 +331,92 @@ async def message_agent(agent_id: str, request: Request, message: str = Form(... ) +# ── Internal HTTP API (Docker container agents) ───────────────────────── + +internal_router = APIRouter(prefix="/internal", tags=["internal"]) + + +class BidRequest(BaseModel): + task_id: str + agent_id: str + bid_sats: int + capabilities: Optional[str] = "" + + +class BidResponse(BaseModel): + accepted: bool + task_id: str + agent_id: str + message: str + + +class TaskSummary(BaseModel): + task_id: str + description: str + status: str + + +@internal_router.get("/tasks", response_model=list[TaskSummary]) +def list_biddable_tasks(): + """Return all tasks currently open for bidding.""" + tasks = coordinator.list_tasks(status=TaskStatus.BIDDING) + return [ + TaskSummary( + task_id=t.id, + description=t.description, + status=t.status.value, + ) + for t in tasks + ] + + +@internal_router.post("/bids", response_model=BidResponse) +def submit_bid(bid: BidRequest): + """Accept a bid from a container agent.""" + if bid.bid_sats <= 0: + raise HTTPException(status_code=422, detail="bid_sats must be > 0") + + accepted = coordinator.auctions.submit_bid( + task_id=bid.task_id, + agent_id=bid.agent_id, + bid_sats=bid.bid_sats, + ) + + if accepted: + from swarm import stats as swarm_stats + swarm_stats.record_bid(bid.task_id, bid.agent_id, bid.bid_sats, won=False) + logger.info( + "Docker agent %s bid %d sats on task %s", + bid.agent_id, bid.bid_sats, bid.task_id, + ) + return BidResponse( + accepted=True, + task_id=bid.task_id, + agent_id=bid.agent_id, + message="Bid accepted.", + ) + + return BidResponse( + accepted=False, + task_id=bid.task_id, + agent_id=bid.agent_id, + message="No open auction for this task — it may have already closed.", + ) + + +# ── WebSocket live feed ────────────────────────────────────────────────── + +@router.websocket("/live") +async def swarm_live(websocket: WebSocket): + """WebSocket endpoint for live swarm event streaming.""" + await ws_manager.connect(websocket) + try: + while True: + data = await websocket.receive_text() + logger.debug("WS received: %s", data[:100]) + except WebSocketDisconnect: + ws_manager.disconnect(websocket) + except Exception as exc: + logger.error("WebSocket error: %s", exc) + ws_manager.disconnect(websocket) + diff --git a/src/dashboard/routes/swarm_internal.py b/src/dashboard/routes/swarm_internal.py deleted file mode 100644 index a079913b..00000000 --- a/src/dashboard/routes/swarm_internal.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Internal swarm HTTP API — for Docker container agents. - -Container agents can't use the in-memory SwarmComms channel, so they poll -these lightweight endpoints to participate in the auction system. - -Routes ------- -GET /internal/tasks - Returns all tasks currently in BIDDING status — the set an agent - can submit bids for. - -POST /internal/bids - Accepts a bid from a container agent and feeds it into the in-memory - AuctionManager. The coordinator then closes auctions and assigns - winners exactly as it does for in-process agents. - -These endpoints are intentionally unauthenticated because they are only -reachable inside the Docker swarm-net bridge network. Do not expose them -through a reverse-proxy to the public internet. -""" - -import logging -from typing import Optional - -from fastapi import APIRouter, HTTPException -from pydantic import BaseModel - -from swarm.coordinator import coordinator -from swarm.tasks import TaskStatus - -logger = logging.getLogger(__name__) - -router = APIRouter(prefix="/internal", tags=["internal"]) - - -# ── Request / response models ───────────────────────────────────────────────── - -class BidRequest(BaseModel): - task_id: str - agent_id: str - bid_sats: int - capabilities: Optional[str] = "" - - -class BidResponse(BaseModel): - accepted: bool - task_id: str - agent_id: str - message: str - - -class TaskSummary(BaseModel): - task_id: str - description: str - status: str - - -# ── Routes ──────────────────────────────────────────────────────────────────── - -@router.get("/tasks", response_model=list[TaskSummary]) -def list_biddable_tasks(): - """Return all tasks currently open for bidding. - - Container agents should poll this endpoint and submit bids for any - tasks they are capable of handling. - """ - tasks = coordinator.list_tasks(status=TaskStatus.BIDDING) - return [ - TaskSummary( - task_id=t.id, - description=t.description, - status=t.status.value, - ) - for t in tasks - ] - - -@router.post("/bids", response_model=BidResponse) -def submit_bid(bid: BidRequest): - """Accept a bid from a container agent. - - The bid is injected directly into the in-memory AuctionManager. - If no auction is open for the task (e.g. it already closed), the - bid is rejected gracefully — the agent should just move on. - """ - if bid.bid_sats <= 0: - raise HTTPException(status_code=422, detail="bid_sats must be > 0") - - accepted = coordinator.auctions.submit_bid( - task_id=bid.task_id, - agent_id=bid.agent_id, - bid_sats=bid.bid_sats, - ) - - if accepted: - # Persist bid in stats table for marketplace analytics - from swarm import stats as swarm_stats - swarm_stats.record_bid(bid.task_id, bid.agent_id, bid.bid_sats, won=False) - logger.info( - "Docker agent %s bid %d sats on task %s", - bid.agent_id, bid.bid_sats, bid.task_id, - ) - return BidResponse( - accepted=True, - task_id=bid.task_id, - agent_id=bid.agent_id, - message="Bid accepted.", - ) - - return BidResponse( - accepted=False, - task_id=bid.task_id, - agent_id=bid.agent_id, - message="No open auction for this task — it may have already closed.", - ) diff --git a/src/dashboard/routes/swarm_ws.py b/src/dashboard/routes/swarm_ws.py deleted file mode 100644 index 13138dd6..00000000 --- a/src/dashboard/routes/swarm_ws.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Swarm WebSocket route — /swarm/live endpoint. - -Provides a real-time WebSocket feed of swarm events for the live -dashboard view. Clients connect and receive JSON events as they -happen: agent joins, task posts, bids, assignments, completions. -""" - -import logging - -from fastapi import APIRouter, WebSocket, WebSocketDisconnect - -from ws_manager.handler import ws_manager - -logger = logging.getLogger(__name__) - -router = APIRouter(tags=["swarm-ws"]) - - -@router.websocket("/swarm/live") -async def swarm_live(websocket: WebSocket): - """WebSocket endpoint for live swarm event streaming.""" - await ws_manager.connect(websocket) - try: - while True: - # Keep the connection alive; client can also send commands - data = await websocket.receive_text() - # Echo back as acknowledgment (future: handle client commands) - logger.debug("WS received: %s", data[:100]) - except WebSocketDisconnect: - ws_manager.disconnect(websocket) - except Exception as exc: - logger.error("WebSocket error: %s", exc) - ws_manager.disconnect(websocket) diff --git a/src/dashboard/routes/voice.py b/src/dashboard/routes/voice.py index 35da0ae0..a5db56d1 100644 --- a/src/dashboard/routes/voice.py +++ b/src/dashboard/routes/voice.py @@ -1,12 +1,17 @@ -"""Voice routes — /voice/* endpoints. +"""Voice routes — /voice/* and /voice/enhanced/* endpoints. -Provides NLU intent detection and TTS control endpoints for the -voice interface. +Provides NLU intent detection, TTS control, and the full voice-to-action +pipeline (detect intent → execute → optionally speak). """ +import logging + from fastapi import APIRouter, Form from voice.nlu import detect_intent, extract_command +from timmy.agent import create_timmy + +logger = logging.getLogger(__name__) router = APIRouter(prefix="/voice", tags=["voice"]) @@ -49,3 +54,103 @@ async def tts_speak(text: str = Form(...)): return {"spoken": True, "text": text} except Exception as exc: return {"spoken": False, "reason": str(exc)} + + +# ── Enhanced voice pipeline ────────────────────────────────────────────── + +@router.post("/enhanced/process") +async def process_voice_input( + text: str = Form(...), + speak_response: bool = Form(False), +): + """Process a voice input: detect intent -> execute -> optionally speak. + + This is the main entry point for voice-driven interaction with Timmy. + """ + intent = detect_intent(text) + response_text = None + error = None + + try: + if intent.name == "status": + response_text = "Timmy is operational and running locally. All systems sovereign." + + elif intent.name == "help": + response_text = ( + "Available commands: chat with me, check status, " + "manage the swarm, create tasks, or adjust voice settings. " + "Everything runs locally — no cloud, no permission needed." + ) + + elif intent.name == "swarm": + from swarm.coordinator import coordinator + status = coordinator.status() + response_text = ( + f"Swarm status: {status['agents']} agents registered, " + f"{status['agents_idle']} idle, {status['agents_busy']} busy. " + f"{status['tasks_total']} total tasks, " + f"{status['tasks_completed']} completed." + ) + + elif intent.name == "voice": + response_text = "Voice settings acknowledged. TTS is available for spoken responses." + + elif intent.name == "code": + from config import settings as app_settings + if not app_settings.self_modify_enabled: + response_text = ( + "Self-modification is disabled. " + "Set SELF_MODIFY_ENABLED=true to enable." + ) + else: + import asyncio + from self_modify.loop import SelfModifyLoop, ModifyRequest + + target_files = [] + if "target_file" in intent.entities: + target_files = [intent.entities["target_file"]] + + loop = SelfModifyLoop() + request = ModifyRequest( + instruction=text, + target_files=target_files, + ) + result = await asyncio.to_thread(loop.run, request) + + if result.success: + sha_short = result.commit_sha[:8] if result.commit_sha else "none" + response_text = ( + f"Code modification complete. " + f"Changed {len(result.files_changed)} file(s). " + f"Tests passed. Committed as {sha_short} " + f"on branch {result.branch_name}." + ) + else: + response_text = f"Code modification failed: {result.error}" + + else: + # Default: chat with Timmy + agent = create_timmy() + run = agent.run(text, stream=False) + response_text = run.content if hasattr(run, "content") else str(run) + + except Exception as exc: + error = f"Processing failed: {exc}" + logger.error("Voice processing error: %s", exc) + + # Optionally speak the response + if speak_response and response_text: + try: + from timmy_serve.voice_tts import voice_tts + if voice_tts.available: + voice_tts.speak(response_text) + except Exception: + pass + + return { + "intent": intent.name, + "confidence": intent.confidence, + "response": response_text, + "error": error, + "spoken": speak_response and response_text is not None, + } diff --git a/src/dashboard/routes/voice_enhanced.py b/src/dashboard/routes/voice_enhanced.py deleted file mode 100644 index 8a17ec01..00000000 --- a/src/dashboard/routes/voice_enhanced.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Enhanced voice routes — /voice/enhanced/* endpoints. - -Combines NLU intent detection with Timmy agent execution to provide -a complete voice-to-action pipeline. Detects the intent, routes to -the appropriate handler, and optionally speaks the response. -""" - -import logging -from typing import Optional - -from fastapi import APIRouter, Form - -from voice.nlu import detect_intent -from timmy.agent import create_timmy - -logger = logging.getLogger(__name__) - -router = APIRouter(prefix="/voice/enhanced", tags=["voice-enhanced"]) - - -@router.post("/process") -async def process_voice_input( - text: str = Form(...), - speak_response: bool = Form(False), -): - """Process a voice input: detect intent → execute → optionally speak. - - This is the main entry point for voice-driven interaction with Timmy. - """ - intent = detect_intent(text) - response_text = None - error = None - - try: - if intent.name == "status": - response_text = "Timmy is operational and running locally. All systems sovereign." - - elif intent.name == "help": - response_text = ( - "Available commands: chat with me, check status, " - "manage the swarm, create tasks, or adjust voice settings. " - "Everything runs locally — no cloud, no permission needed." - ) - - elif intent.name == "swarm": - from swarm.coordinator import coordinator - status = coordinator.status() - response_text = ( - f"Swarm status: {status['agents']} agents registered, " - f"{status['agents_idle']} idle, {status['agents_busy']} busy. " - f"{status['tasks_total']} total tasks, " - f"{status['tasks_completed']} completed." - ) - - elif intent.name == "voice": - response_text = "Voice settings acknowledged. TTS is available for spoken responses." - - elif intent.name == "code": - from config import settings as app_settings - if not app_settings.self_modify_enabled: - response_text = ( - "Self-modification is disabled. " - "Set SELF_MODIFY_ENABLED=true to enable." - ) - else: - import asyncio - from self_modify.loop import SelfModifyLoop, ModifyRequest - - target_files = [] - if "target_file" in intent.entities: - target_files = [intent.entities["target_file"]] - - loop = SelfModifyLoop() - request = ModifyRequest( - instruction=text, - target_files=target_files, - ) - result = await asyncio.to_thread(loop.run, request) - - if result.success: - sha_short = result.commit_sha[:8] if result.commit_sha else "none" - response_text = ( - f"Code modification complete. " - f"Changed {len(result.files_changed)} file(s). " - f"Tests passed. Committed as {sha_short} " - f"on branch {result.branch_name}." - ) - else: - response_text = f"Code modification failed: {result.error}" - - else: - # Default: chat with Timmy - agent = create_timmy() - run = agent.run(text, stream=False) - response_text = run.content if hasattr(run, "content") else str(run) - - except Exception as exc: - error = f"Processing failed: {exc}" - logger.error("Voice processing error: %s", exc) - - # Optionally speak the response - if speak_response and response_text: - try: - from timmy_serve.voice_tts import voice_tts - if voice_tts.available: - voice_tts.speak(response_text) - except Exception: - pass - - return { - "intent": intent.name, - "confidence": intent.confidence, - "response": response_text, - "error": error, - "spoken": speak_response and response_text is not None, - } diff --git a/src/dashboard/templates/mobile_test.html b/src/dashboard/templates/mobile_test.html deleted file mode 100644 index c6d31cca..00000000 --- a/src/dashboard/templates/mobile_test.html +++ /dev/null @@ -1,422 +0,0 @@ -{% extends "base.html" %} -{% block title %}Mobile Test — Timmy Time{% endblock %} - -{% block content %} -
- - -
-
- // MOBILE TEST SUITE - HUMAN-IN-THE-LOOP -
-
- 0 / {{ total }} - PASSED -
-
- - -
-
-
-
-
- PASS - FAIL - SKIP - PENDING -
-
- - -
- ← MISSION CONTROL - -
- - - {% for category, items in categories.items() %} -
{{ category | upper }}
- - {% for s in items %} -
-
-
- {{ s.id }} - {{ s.title }} -
- PENDING -
-
- -
STEPS
-
    - {% for step in s.steps %} -
  1. {{ step }}
  2. - {% endfor %} -
- -
EXPECTED
-
{{ s.expected }}
- -
- - - -
- -
-
- {% endfor %} - {% endfor %} - - -
-
// SUMMARY
-
-

Mark all scenarios above to see your final score.

-
-
- -
- - - - - - - - - -{% endblock %} diff --git a/tests/integrations/test_voice_enhanced.py b/tests/integrations/test_voice_enhanced.py index 0ed802db..a4b53cd0 100644 --- a/tests/integrations/test_voice_enhanced.py +++ b/tests/integrations/test_voice_enhanced.py @@ -1,4 +1,4 @@ -"""Tests for dashboard/routes/voice_enhanced.py — enhanced voice processing.""" +"""Tests for enhanced voice processing (merged into dashboard/routes/voice.py).""" from unittest.mock import MagicMock, patch @@ -56,7 +56,7 @@ class TestVoiceEnhancedProcess: mock_run.content = "Hello from Timmy!" mock_agent.run.return_value = mock_run - with patch("dashboard.routes.voice_enhanced.create_timmy", return_value=mock_agent): + with patch("dashboard.routes.voice.create_timmy", return_value=mock_agent): resp = client.post( "/voice/enhanced/process", data={"text": "tell me about Bitcoin", "speak_response": "false"}, @@ -69,7 +69,7 @@ class TestVoiceEnhancedProcess: def test_chat_fallback_error_handling(self, client): """When the agent raises, the error should be captured gracefully.""" with patch( - "dashboard.routes.voice_enhanced.create_timmy", + "dashboard.routes.voice.create_timmy", side_effect=RuntimeError("Ollama offline"), ): resp = client.post( diff --git a/tests/security/test_xss_prevention.py b/tests/security/test_xss_prevention.py deleted file mode 100644 index f1d65499..00000000 --- a/tests/security/test_xss_prevention.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Regression tests for XSS prevention in the dashboard.""" - -import pytest -from fastapi.testclient import TestClient - -def test_mobile_test_page_xss_prevention(client: TestClient): - """ - Verify that the mobile-test page uses safer DOM manipulation. - This test checks the template content for the presence of textContent - and proper usage of innerHTML for known safe constants. - """ - response = client.get("/mobile-test") - assert response.status_code == 200 - content = response.text - - # Check that we are using textContent for dynamic content - assert "textContent =" in content - - # Check that we've updated the summaryBody.innerHTML usage to be safer - # or replaced with appendChild/textContent where appropriate. - # The fix uses innerHTML with template literals for structural parts - # but textContent for data parts. - assert "summaryBody.innerHTML = '';" in content - assert "p.textContent =" in content - assert "statusMsg.textContent =" in content