refactor: Phase 2a — consolidate dashboard routes (27→22 files)

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
This commit is contained in:
Claude
2026-02-26 21:30:39 +00:00
parent 4e11dd2490
commit d2c80fbf4c
13 changed files with 275 additions and 1061 deletions

View File

@@ -130,7 +130,6 @@ markers = [
source = ["src"]
omit = [
"*/tests/*",
"src/dashboard/routes/mobile_test.py",
]
[tool.coverage.report]

View File

@@ -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)

View File

@@ -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 <your name>.'",
"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)},
)

View File

@@ -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}

View File

@@ -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}

View File

@@ -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)

View File

@@ -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.",
)

View File

@@ -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)

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -1,422 +0,0 @@
{% extends "base.html" %}
{% block title %}Mobile Test — Timmy Time{% endblock %}
{% block content %}
<div class="container-fluid mc-content" style="height:auto; overflow:visible;">
<!-- ── Page header ─────────────────────────────────────────────────── -->
<div class="mt-hitl-header">
<div>
<span class="mt-title">// MOBILE TEST SUITE</span>
<span class="mt-sub">HUMAN-IN-THE-LOOP</span>
</div>
<div class="mt-score-wrap">
<span class="mt-score" id="score-display">0 / {{ total }}</span>
<span class="mt-score-label">PASSED</span>
</div>
</div>
<!-- ── Progress bar ────────────────────────────────────────────────── -->
<div class="mt-progress-wrap">
<div class="progress" style="height:6px; background:var(--bg-card); border-radius:3px;">
<div class="progress-bar mt-progress-bar"
id="progress-bar"
role="progressbar"
style="width:0%; background:var(--green);"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="{{ total }}"></div>
</div>
<div class="mt-progress-legend">
<span><span class="mt-dot green"></span>PASS</span>
<span><span class="mt-dot red"></span>FAIL</span>
<span><span class="mt-dot amber"></span>SKIP</span>
<span><span class="mt-dot" style="background:var(--text-dim);box-shadow:none;"></span>PENDING</span>
</div>
</div>
<!-- ── Reset / Back ────────────────────────────────────────────────── -->
<div class="mt-actions">
<a href="/" class="mc-btn-clear">← MISSION CONTROL</a>
<button class="mc-btn-clear" onclick="resetAll()" style="border-color:var(--red);color:var(--red);">RESET ALL</button>
</div>
<!-- ── Scenario cards ──────────────────────────────────────────────── -->
{% for category, items in categories.items() %}
<div class="mt-category-label">{{ category | upper }}</div>
{% for s in items %}
<div class="card mc-panel mt-card" id="card-{{ s.id }}" data-scenario="{{ s.id }}">
<div class="card-header mc-panel-header d-flex justify-content-between align-items-center">
<div>
<span class="mt-id-badge" id="badge-{{ s.id }}">{{ s.id }}</span>
<span class="mt-scenario-title">{{ s.title }}</span>
</div>
<span class="mt-state-chip" id="chip-{{ s.id }}">PENDING</span>
</div>
<div class="card-body p-3">
<div class="mt-steps-label">STEPS</div>
<ol class="mt-steps">
{% for step in s.steps %}
<li>{{ step }}</li>
{% endfor %}
</ol>
<div class="mt-expected-label">EXPECTED</div>
<div class="mt-expected">{{ s.expected }}</div>
<div class="mt-btn-row">
<button class="mt-btn mt-btn-pass" onclick="mark('{{ s.id }}', 'pass')">✓ PASS</button>
<button class="mt-btn mt-btn-fail" onclick="mark('{{ s.id }}', 'fail')">✗ FAIL</button>
<button class="mt-btn mt-btn-skip" onclick="mark('{{ s.id }}', 'skip')">— SKIP</button>
</div>
</div>
</div>
{% endfor %}
{% endfor %}
<!-- ── Summary footer ──────────────────────────────────────────────── -->
<div class="card mc-panel mt-summary" id="summary">
<div class="card-header mc-panel-header">// SUMMARY</div>
<div class="card-body p-3" id="summary-body">
<p class="mt-summary-hint">Mark all scenarios above to see your final score.</p>
</div>
</div>
</div><!-- /container -->
<!-- ── Styles (scoped to this page) ────────────────────────────────────── -->
<style>
.mt-hitl-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
padding: 16px 0 12px;
border-bottom: 1px solid var(--border);
margin-bottom: 12px;
}
.mt-title {
font-size: 14px;
font-weight: 700;
color: var(--text-bright);
letter-spacing: 0.18em;
display: block;
}
.mt-sub {
font-size: 10px;
color: var(--text-dim);
letter-spacing: 0.2em;
display: block;
margin-top: 2px;
}
.mt-score-wrap { text-align: right; }
.mt-score {
font-size: 22px;
font-weight: 700;
color: var(--green);
letter-spacing: 0.06em;
display: block;
}
.mt-score-label { font-size: 9px; color: var(--text-dim); letter-spacing: 0.2em; }
.mt-progress-wrap { margin-bottom: 10px; }
.mt-progress-legend {
display: flex;
gap: 16px;
font-size: 9px;
color: var(--text-dim);
letter-spacing: 0.12em;
margin-top: 6px;
}
.mt-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
margin-right: 4px;
vertical-align: middle;
}
.mt-dot.green { background: var(--green); box-shadow: 0 0 5px var(--green); }
.mt-dot.red { background: var(--red); box-shadow: 0 0 5px var(--red); }
.mt-dot.amber { background: var(--amber); box-shadow: 0 0 5px var(--amber); }
.mt-actions {
display: flex;
gap: 10px;
margin-bottom: 16px;
}
.mt-category-label {
font-size: 9px;
font-weight: 700;
color: var(--text-dim);
letter-spacing: 0.25em;
margin: 20px 0 8px;
padding-left: 2px;
}
.mt-card {
margin-bottom: 10px;
transition: border-color 0.2s;
}
.mt-card.state-pass { border-color: var(--green) !important; }
.mt-card.state-fail { border-color: var(--red) !important; }
.mt-card.state-skip { border-color: var(--amber) !important; opacity: 0.7; }
.mt-id-badge {
font-size: 9px;
font-weight: 700;
background: var(--border);
color: var(--text-dim);
border-radius: 2px;
padding: 2px 6px;
letter-spacing: 0.12em;
margin-right: 8px;
}
.mt-card.state-pass .mt-id-badge { background: var(--green-dim); color: var(--green); }
.mt-card.state-fail .mt-id-badge { background: var(--red-dim); color: var(--red); }
.mt-card.state-skip .mt-id-badge { background: var(--amber-dim); color: var(--amber); }
.mt-scenario-title {
font-size: 12px;
font-weight: 700;
color: var(--text-bright);
letter-spacing: 0.05em;
}
.mt-state-chip {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.15em;
color: var(--text-dim);
padding: 2px 8px;
border: 1px solid var(--border);
border-radius: 2px;
white-space: nowrap;
}
.mt-card.state-pass .mt-state-chip { color: var(--green); border-color: var(--green); }
.mt-card.state-fail .mt-state-chip { color: var(--red); border-color: var(--red); }
.mt-card.state-skip .mt-state-chip { color: var(--amber); border-color: var(--amber); }
.mt-steps-label, .mt-expected-label {
font-size: 9px;
font-weight: 700;
color: var(--text-dim);
letter-spacing: 0.2em;
margin-bottom: 6px;
}
.mt-expected-label { margin-top: 12px; }
.mt-steps {
padding-left: 18px;
margin: 0;
font-size: 12px;
line-height: 1.8;
color: var(--text);
}
.mt-expected {
font-size: 12px;
line-height: 1.65;
color: var(--text-bright);
background: var(--bg-card);
border-left: 3px solid var(--border-glow);
padding: 8px 12px;
border-radius: 0 3px 3px 0;
}
.mt-btn-row {
display: flex;
gap: 8px;
margin-top: 14px;
}
.mt-btn {
flex: 1;
min-height: 44px;
border: 1px solid var(--border);
border-radius: 3px;
background: var(--bg-deep);
color: var(--text-dim);
font-family: var(--font);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
cursor: pointer;
touch-action: manipulation;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.mt-btn-pass:hover, .mt-btn-pass.active { background: var(--green-dim); color: var(--green); border-color: var(--green); }
.mt-btn-fail:hover, .mt-btn-fail.active { background: var(--red-dim); color: var(--red); border-color: var(--red); }
.mt-btn-skip:hover, .mt-btn-skip.active { background: var(--amber-dim); color: var(--amber); border-color: var(--amber); }
.mt-summary { margin-top: 24px; margin-bottom: 32px; }
.mt-summary-hint { color: var(--text-dim); font-size: 12px; margin: 0; }
.mt-summary-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid var(--border);
font-size: 12px;
}
.mt-summary-row:last-child { border-bottom: none; }
.mt-summary-score { font-size: 28px; font-weight: 700; color: var(--green); margin: 12px 0 4px; }
.mt-summary-pct { font-size: 13px; color: var(--text-dim); }
@media (max-width: 768px) {
.mt-btn-row { gap: 6px; }
.mt-btn { font-size: 10px; padding: 0 4px; }
}
</style>
<!-- ── HITL State Machine (sessionStorage) ─────────────────────────────── -->
<script>
const TOTAL = {{ total }};
const KEY = "timmy-mobile-test-results";
function loadResults() {
try { return JSON.parse(sessionStorage.getItem(KEY) || "{}"); }
catch { return {}; }
}
function saveResults(r) {
sessionStorage.setItem(KEY, JSON.stringify(r));
}
function mark(id, state) {
const results = loadResults();
results[id] = state;
saveResults(results);
applyState(id, state);
updateScore(results);
updateSummary(results);
}
function applyState(id, state) {
const card = document.getElementById("card-" + id);
const chip = document.getElementById("chip-" + id);
const labels = { pass: "PASS", fail: "FAIL", skip: "SKIP" };
card.classList.remove("state-pass", "state-fail", "state-skip");
if (state) card.classList.add("state-" + state);
chip.textContent = state ? labels[state] : "PENDING";
// highlight active button
card.querySelectorAll(".mt-btn").forEach(btn => btn.classList.remove("active"));
const activeBtn = card.querySelector(".mt-btn-" + state);
if (activeBtn) activeBtn.classList.add("active");
}
function updateScore(results) {
const passed = Object.values(results).filter(v => v === "pass").length;
const decided = Object.values(results).filter(v => v !== undefined).length;
document.getElementById("score-display").textContent = passed + " / " + TOTAL;
const pct = TOTAL ? (decided / TOTAL) * 100 : 0;
const bar = document.getElementById("progress-bar");
bar.style.width = pct + "%";
// colour the bar by overall health
const failCount = Object.values(results).filter(v => v === "fail").length;
bar.style.background = failCount > 0
? "var(--red)"
: passed === TOTAL ? "var(--green)" : "var(--amber)";
}
function updateSummary(results) {
const passed = Object.values(results).filter(v => v === "pass").length;
const failed = Object.values(results).filter(v => v === "fail").length;
const skipped = Object.values(results).filter(v => v === "skip").length;
const decided = passed + failed + skipped;
const summaryBody = document.getElementById("summary-body");
if (decided < TOTAL) {
summaryBody.innerHTML = '';
const p = document.createElement('p');
p.className = 'mt-summary-hint';
p.textContent = (TOTAL - decided) + ' scenario(s) still pending.';
summaryBody.appendChild(p);
return;
}
const pct = TOTAL ? Math.round((passed / TOTAL) * 100) : 0;
const color = failed > 0 ? "var(--red)" : "var(--green)";
// Safely build summary UI using DOM API to avoid XSS from potentially untrusted variables
summaryBody.innerHTML = '';
const scoreDiv = document.createElement('div');
scoreDiv.className = 'mt-summary-score';
scoreDiv.style.color = color;
scoreDiv.textContent = passed + ' / ' + TOTAL;
summaryBody.appendChild(scoreDiv);
const pctDiv = document.createElement('div');
pctDiv.className = 'mt-summary-pct';
pctDiv.textContent = pct + '% pass rate';
summaryBody.appendChild(pctDiv);
const statsContainer = document.createElement('div');
statsContainer.style.marginTop = '16px';
const createRow = (label, value, colorVar) => {
const row = document.createElement('div');
row.className = 'mt-summary-row';
const labelSpan = document.createElement('span');
labelSpan.textContent = label;
const valSpan = document.createElement('span');
valSpan.style.color = 'var(--' + colorVar + ')';
valSpan.style.fontWeight = '700';
valSpan.textContent = value;
row.appendChild(labelSpan);
row.appendChild(valSpan);
return row;
};
statsContainer.appendChild(createRow('PASSED', passed, 'green'));
statsContainer.appendChild(createRow('FAILED', failed, 'red'));
statsContainer.appendChild(createRow('SKIPPED', skipped, 'amber'));
summaryBody.appendChild(statsContainer);
const statusMsg = document.createElement('p');
statusMsg.style.marginTop = '12px';
statusMsg.style.fontSize = '11px';
if (failed > 0) {
statusMsg.style.color = 'var(--red)';
statusMsg.textContent = '⚠ ' + failed + ' failure(s) need attention before release.';
} else {
statusMsg.style.color = 'var(--green)';
statusMsg.textContent = 'All tested scenarios passed — ship it.';
}
summaryBody.appendChild(statusMsg);
}
function resetAll() {
if (!confirm("Reset all test results?")) return;
sessionStorage.removeItem(KEY);
const results = {};
document.querySelectorAll("[data-scenario]").forEach(card => {
const id = card.dataset.scenario;
applyState(id, null);
});
updateScore(results);
const summaryBody = document.getElementById("summary-body");
summaryBody.innerHTML = '';
const p = document.createElement('p');
p.className = 'mt-summary-hint';
p.textContent = 'Mark all scenarios above to see your final score.';
summaryBody.appendChild(p);
}
// Restore saved state on load
(function init() {
const results = loadResults();
Object.entries(results).forEach(([id, state]) => applyState(id, state));
updateScore(results);
updateSummary(results);
})();
</script>
{% endblock %}

View File

@@ -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(

View File

@@ -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