forked from Rockachopa/Timmy-time-dashboard
fix: Docker-first test suite, UX improvements, and bug fixes (#100)
Dashboard UX: - Restructure nav from 22 flat links to 6 core + MORE dropdown - Add mobile nav section labels (Core, Intelligence, Agents, System, Commerce) - Defer marked.js and dompurify.js loading, consolidate CDN to jsdelivr - Optimize font weights (drop unused 300/500), bump style.css cache buster - Remove duplicate HTMX load triggers from sidebar and health panels Bug fixes: - Fix Timmy showing OFFLINE by registering after swarm recovery sweep - Fix ThinkingEngine await bug with asyncio.run_coroutine_threadsafe - Fix chat auto-scroll by calling scrollChat() after history partial loads - Add missing /voice/button page and /voice/command endpoint - Fix Grok api_key="" treated as falsy falling through to env key - Fix self_modify PROJECT_ROOT using settings.repo_root instead of __file__ Docker test infrastructure: - Bind-mount hands/, docker/, Dockerfiles, and compose files into test container - Add fontconfig + fonts-dejavu-core for creative/assembler TextClip tests - Initialize minimal git repo in Dockerfile.test for GitSafety compatibility - Fix introspection and path resolution tests for Docker /app context All 1863 tests pass in Docker (0 failures, 77 skipped). Co-authored-by: Alexander Payne <apayne@MM.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
6e67c3b421
commit
89cfe1be0d
@@ -39,6 +39,14 @@ services:
|
|||||||
- ./src:/app/src:ro
|
- ./src:/app/src:ro
|
||||||
- ./tests:/app/tests:ro
|
- ./tests:/app/tests:ro
|
||||||
- ./static:/app/static:ro
|
- ./static:/app/static:ro
|
||||||
|
- ./hands:/app/hands:ro
|
||||||
|
- ./docker:/app/docker:ro
|
||||||
|
- ./Dockerfile:/app/Dockerfile:ro
|
||||||
|
- ./docker-compose.yml:/app/docker-compose.yml:ro
|
||||||
|
- ./docker-compose.dev.yml:/app/docker-compose.dev.yml:ro
|
||||||
|
- ./docker-compose.prod.yml:/app/docker-compose.prod.yml:ro
|
||||||
|
- ./docker-compose.test.yml:/app/docker-compose.test.yml:ro
|
||||||
|
- ./docker-compose.microservices.yml:/app/docker-compose.microservices.yml:ro
|
||||||
- ./pyproject.toml:/app/pyproject.toml:ro
|
- ./pyproject.toml:/app/pyproject.toml:ro
|
||||||
- test-data:/app/data
|
- test-data:/app/data
|
||||||
environment:
|
environment:
|
||||||
@@ -67,6 +75,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./src:/app/src:ro
|
- ./src:/app/src:ro
|
||||||
- ./static:/app/static:ro
|
- ./static:/app/static:ro
|
||||||
|
- ./hands:/app/hands:ro
|
||||||
- test-data:/app/data
|
- test-data:/app/data
|
||||||
environment:
|
environment:
|
||||||
DEBUG: "true"
|
DEBUG: "true"
|
||||||
@@ -117,6 +126,7 @@ services:
|
|||||||
- agents
|
- agents
|
||||||
volumes:
|
volumes:
|
||||||
- ./src:/app/src:ro
|
- ./src:/app/src:ro
|
||||||
|
- ./hands:/app/hands:ro
|
||||||
- test-data:/app/data
|
- test-data:/app/data
|
||||||
environment:
|
environment:
|
||||||
COORDINATOR_URL: "http://dashboard:8000"
|
COORDINATOR_URL: "http://dashboard:8000"
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ FROM python:3.12-slim
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
curl git \
|
curl git fontconfig fonts-dejavu-core \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy installed packages from builder
|
# Copy installed packages from builder
|
||||||
@@ -48,7 +48,14 @@ COPY --from=builder /usr/local/lib/python3.12/site-packages \
|
|||||||
COPY --from=builder /usr/local/bin /usr/local/bin
|
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||||
|
|
||||||
# Create directories for bind mounts
|
# Create directories for bind mounts
|
||||||
RUN mkdir -p /app/src /app/tests /app/static /app/data
|
RUN mkdir -p /app/src /app/tests /app/static /app/hands /app/data /app/docker
|
||||||
|
|
||||||
|
# Initialize a minimal git repo so git-dependent code (GitSafety, repo_root
|
||||||
|
# detection) works correctly inside the container.
|
||||||
|
RUN git config --global user.email "timmy@test" \
|
||||||
|
&& git config --global user.name "Timmy Test" \
|
||||||
|
&& git init /app \
|
||||||
|
&& git -C /app commit --allow-empty -m "init"
|
||||||
|
|
||||||
ENV PYTHONPATH=/app/src:/app/tests
|
ENV PYTHONPATH=/app/src:/app/tests
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|||||||
@@ -275,7 +275,11 @@ async def _task_processor_loop() -> None:
|
|||||||
def handle_thought(task):
|
def handle_thought(task):
|
||||||
from timmy.thinking import thinking_engine
|
from timmy.thinking import thinking_engine
|
||||||
try:
|
try:
|
||||||
result = thinking_engine.think_once()
|
loop = asyncio.get_event_loop()
|
||||||
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
thinking_engine.think_once(), loop
|
||||||
|
)
|
||||||
|
result = future.result(timeout=120)
|
||||||
return str(result) if result else "Thought completed"
|
return str(result) if result else "Thought completed"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Thought processing failed: %s", e)
|
logger.error("Thought processing failed: %s", e)
|
||||||
@@ -457,15 +461,7 @@ async def lifespan(app: FastAPI):
|
|||||||
# Create all background tasks without waiting for them
|
# Create all background tasks without waiting for them
|
||||||
briefing_task = asyncio.create_task(_briefing_scheduler())
|
briefing_task = asyncio.create_task(_briefing_scheduler())
|
||||||
|
|
||||||
# Register Timmy in swarm registry
|
# Run swarm recovery first (offlines all stale agents)
|
||||||
from swarm import registry as swarm_registry
|
|
||||||
swarm_registry.register(
|
|
||||||
name="Timmy",
|
|
||||||
capabilities="chat,reasoning,research,planning",
|
|
||||||
agent_id="timmy",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Run swarm recovery and log summary
|
|
||||||
from swarm.coordinator import coordinator as swarm_coordinator
|
from swarm.coordinator import coordinator as swarm_coordinator
|
||||||
swarm_coordinator.initialize()
|
swarm_coordinator.initialize()
|
||||||
rec = swarm_coordinator._recovery_summary
|
rec = swarm_coordinator._recovery_summary
|
||||||
@@ -476,6 +472,14 @@ async def lifespan(app: FastAPI):
|
|||||||
rec["agents_offlined"],
|
rec["agents_offlined"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Register Timmy AFTER recovery sweep so status sticks as "idle"
|
||||||
|
from swarm import registry as swarm_registry
|
||||||
|
swarm_registry.register(
|
||||||
|
name="Timmy",
|
||||||
|
capabilities="chat,reasoning,research,planning",
|
||||||
|
agent_id="timmy",
|
||||||
|
)
|
||||||
|
|
||||||
# Spawn persona agents in background
|
# Spawn persona agents in background
|
||||||
persona_task = asyncio.create_task(_spawn_persona_agents_background())
|
persona_task = asyncio.create_task(_spawn_persona_agents_background())
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
"""Voice routes — /voice/* and /voice/enhanced/* endpoints.
|
"""Voice routes — /voice/* and /voice/enhanced/* endpoints.
|
||||||
|
|
||||||
Provides NLU intent detection, TTS control, and the full voice-to-action
|
Provides NLU intent detection, TTS control, the full voice-to-action
|
||||||
pipeline (detect intent → execute → optionally speak).
|
pipeline (detect intent → execute → optionally speak), and the voice
|
||||||
|
button UI page.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from fastapi import APIRouter, Form
|
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 integrations.voice.nlu import detect_intent, extract_command
|
||||||
from timmy.agent import create_timmy
|
from timmy.agent import create_timmy
|
||||||
@@ -14,6 +18,7 @@ from timmy.agent import create_timmy
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter(prefix="/voice", tags=["voice"])
|
router = APIRouter(prefix="/voice", tags=["voice"])
|
||||||
|
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/nlu")
|
@router.post("/nlu")
|
||||||
@@ -56,6 +61,31 @@ async def tts_speak(text: str = Form(...)):
|
|||||||
return {"spoken": False, "reason": str(exc)}
|
return {"spoken": False, "reason": str(exc)}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Voice button page ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/button", response_class=HTMLResponse)
|
||||||
|
async def voice_button_page(request: Request):
|
||||||
|
"""Render the voice button UI."""
|
||||||
|
return templates.TemplateResponse(request, "voice_button.html")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/command")
|
||||||
|
async def voice_command(text: str = Form(...)):
|
||||||
|
"""Process a voice command (used by voice_button.html).
|
||||||
|
|
||||||
|
Wraps the enhanced pipeline and returns the result in the format
|
||||||
|
the voice button template expects.
|
||||||
|
"""
|
||||||
|
result = await process_voice_input(text=text, speak_response=False)
|
||||||
|
return {
|
||||||
|
"command": {
|
||||||
|
"intent": result["intent"],
|
||||||
|
"response": result["response"] or result.get("error", "No response"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ── Enhanced voice pipeline ──────────────────────────────────────────────
|
# ── Enhanced voice pipeline ──────────────────────────────────────────────
|
||||||
|
|
||||||
@router.post("/enhanced/process")
|
@router.post("/enhanced/process")
|
||||||
|
|||||||
@@ -9,13 +9,14 @@
|
|||||||
<title>{% block title %}Timmy Time — Mission Control{% endblock %}</title>
|
<title>{% block title %}Timmy Time — Mission Control{% endblock %}</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet" />
|
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet" />
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous" />
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous" />
|
||||||
<link rel="stylesheet" href="/static/style.css?v=4" />
|
<link rel="stylesheet" href="/static/style.css?v=5" />
|
||||||
{% block extra_styles %}{% endblock %}
|
{% block extra_styles %}{% endblock %}
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.3/dist/htmx.min.js" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.3/dist/htmx.min.js" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="mc-header">
|
<header class="mc-header">
|
||||||
@@ -28,26 +29,31 @@
|
|||||||
<div class="mc-header-right mc-desktop-nav">
|
<div class="mc-header-right mc-desktop-nav">
|
||||||
<a href="/tasks" class="mc-test-link">TASKS</a>
|
<a href="/tasks" class="mc-test-link">TASKS</a>
|
||||||
<a href="/briefing" class="mc-test-link">BRIEFING</a>
|
<a href="/briefing" class="mc-test-link">BRIEFING</a>
|
||||||
<a href="/thinking" class="mc-test-link" style="color:#c084fc;">THINKING</a>
|
<a href="/thinking" class="mc-test-link mc-link-thinking">THINKING</a>
|
||||||
<a href="/swarm/mission-control" class="mc-test-link">MISSION CONTROL</a>
|
<a href="/swarm/mission-control" class="mc-test-link">MISSION CTRL</a>
|
||||||
<a href="/swarm/live" class="mc-test-link">SWARM</a>
|
<a href="/swarm/live" class="mc-test-link">SWARM</a>
|
||||||
<a href="/spark/ui" class="mc-test-link">SPARK</a>
|
<a href="/bugs" class="mc-test-link mc-link-bugs">BUGS</a>
|
||||||
<a href="/marketplace/ui" class="mc-test-link">MARKET</a>
|
<div class="mc-nav-dropdown">
|
||||||
<a href="/tools" class="mc-test-link">TOOLS</a>
|
<button class="mc-test-link mc-dropdown-toggle" aria-expanded="false">MORE ▾</button>
|
||||||
<a href="/swarm/events" class="mc-test-link">EVENTS</a>
|
<div class="mc-dropdown-menu">
|
||||||
<a href="/bugs" class="mc-test-link" style="color:#ff6b6b;">BUGS</a>
|
<a href="/spark/ui" class="mc-test-link">SPARK</a>
|
||||||
<a href="/lightning/ledger" class="mc-test-link">LEDGER</a>
|
<a href="/marketplace/ui" class="mc-test-link">MARKET</a>
|
||||||
<a href="/memory" class="mc-test-link">MEMORY</a>
|
<a href="/tools" class="mc-test-link">TOOLS</a>
|
||||||
<a href="/router/status" class="mc-test-link">ROUTER</a>
|
<a href="/swarm/events" class="mc-test-link">EVENTS</a>
|
||||||
<a href="/grok/status" class="mc-test-link" style="color:#00ff88;">GROK</a>
|
<a href="/lightning/ledger" class="mc-test-link">LEDGER</a>
|
||||||
<a href="/self-modify/queue" class="mc-test-link">UPGRADES</a>
|
<a href="/memory" class="mc-test-link">MEMORY</a>
|
||||||
<a href="/self-coding" class="mc-test-link">SELF-CODING</a>
|
<a href="/router/status" class="mc-test-link">ROUTER</a>
|
||||||
<a href="/hands" class="mc-test-link">HANDS</a>
|
<a href="/grok/status" class="mc-test-link mc-link-grok">GROK</a>
|
||||||
<a href="/work-orders/queue" class="mc-test-link">WORK ORDERS</a>
|
<a href="/self-modify/queue" class="mc-test-link">UPGRADES</a>
|
||||||
<a href="/creative/ui" class="mc-test-link">CREATIVE</a>
|
<a href="/self-coding" class="mc-test-link">SELF-CODING</a>
|
||||||
<a href="/voice/button" class="mc-test-link">VOICE</a>
|
<a href="/hands" class="mc-test-link">HANDS</a>
|
||||||
<a href="/mobile" class="mc-test-link" title="Mobile-optimized view">MOBILE</a>
|
<a href="/work-orders/queue" class="mc-test-link">WORK ORDERS</a>
|
||||||
<a href="/mobile/local" class="mc-test-link" title="Local AI on iPhone">LOCAL AI</a>
|
<a href="/creative/ui" class="mc-test-link">CREATIVE</a>
|
||||||
|
<a href="/voice/button" class="mc-test-link">VOICE</a>
|
||||||
|
<a href="/mobile" class="mc-test-link" title="Mobile-optimized view">MOBILE</a>
|
||||||
|
<a href="/mobile/local" class="mc-test-link" title="Local AI on iPhone">LOCAL AI</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button id="enable-notifications" class="mc-test-link" style="background:none;cursor:pointer;" title="Enable notifications">🔔</button>
|
<button id="enable-notifications" class="mc-test-link" style="background:none;cursor:pointer;" title="Enable notifications">🔔</button>
|
||||||
<span class="mc-time" id="clock"></span>
|
<span class="mc-time" id="clock"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,24 +72,29 @@
|
|||||||
<span class="mc-time" id="clock-mobile"></span>
|
<span class="mc-time" id="clock-mobile"></span>
|
||||||
</div>
|
</div>
|
||||||
<a href="/" class="mc-mobile-link">HOME</a>
|
<a href="/" class="mc-mobile-link">HOME</a>
|
||||||
|
<div class="mc-mobile-section-label">CORE</div>
|
||||||
<a href="/tasks" class="mc-mobile-link">TASKS</a>
|
<a href="/tasks" class="mc-mobile-link">TASKS</a>
|
||||||
<a href="/briefing" class="mc-mobile-link">BRIEFING</a>
|
<a href="/briefing" class="mc-mobile-link">BRIEFING</a>
|
||||||
<a href="/thinking" class="mc-mobile-link">THINKING</a>
|
<a href="/thinking" class="mc-mobile-link">THINKING</a>
|
||||||
<a href="/swarm/mission-control" class="mc-mobile-link">MISSION CONTROL</a>
|
<a href="/swarm/mission-control" class="mc-mobile-link">MISSION CONTROL</a>
|
||||||
<a href="/swarm/live" class="mc-mobile-link">SWARM</a>
|
<a href="/swarm/live" class="mc-mobile-link">SWARM</a>
|
||||||
<a href="/spark/ui" class="mc-mobile-link">SPARK</a>
|
|
||||||
<a href="/marketplace/ui" class="mc-mobile-link">MARKET</a>
|
|
||||||
<a href="/tools" class="mc-mobile-link">TOOLS</a>
|
|
||||||
<a href="/swarm/events" class="mc-mobile-link">EVENTS</a>
|
|
||||||
<a href="/bugs" class="mc-mobile-link">BUGS</a>
|
<a href="/bugs" class="mc-mobile-link">BUGS</a>
|
||||||
<a href="/lightning/ledger" class="mc-mobile-link">LEDGER</a>
|
<div class="mc-mobile-section-label">INTELLIGENCE</div>
|
||||||
|
<a href="/spark/ui" class="mc-mobile-link">SPARK</a>
|
||||||
<a href="/memory" class="mc-mobile-link">MEMORY</a>
|
<a href="/memory" class="mc-mobile-link">MEMORY</a>
|
||||||
<a href="/router/status" class="mc-mobile-link">ROUTER</a>
|
<a href="/marketplace/ui" class="mc-mobile-link">MARKET</a>
|
||||||
<a href="/grok/status" class="mc-mobile-link">GROK</a>
|
<div class="mc-mobile-section-label">AGENTS</div>
|
||||||
<a href="/self-modify/queue" class="mc-mobile-link">UPGRADES</a>
|
|
||||||
<a href="/self-coding" class="mc-mobile-link">SELF-CODING</a>
|
|
||||||
<a href="/hands" class="mc-mobile-link">HANDS</a>
|
<a href="/hands" class="mc-mobile-link">HANDS</a>
|
||||||
<a href="/work-orders/queue" class="mc-mobile-link">WORK ORDERS</a>
|
<a href="/work-orders/queue" class="mc-mobile-link">WORK ORDERS</a>
|
||||||
|
<a href="/self-modify/queue" class="mc-mobile-link">UPGRADES</a>
|
||||||
|
<a href="/self-coding" class="mc-mobile-link">SELF-CODING</a>
|
||||||
|
<div class="mc-mobile-section-label">SYSTEM</div>
|
||||||
|
<a href="/tools" class="mc-mobile-link">TOOLS</a>
|
||||||
|
<a href="/swarm/events" class="mc-mobile-link">EVENTS</a>
|
||||||
|
<a href="/router/status" class="mc-mobile-link">ROUTER</a>
|
||||||
|
<a href="/grok/status" class="mc-mobile-link">GROK</a>
|
||||||
|
<div class="mc-mobile-section-label">COMMERCE</div>
|
||||||
|
<a href="/lightning/ledger" class="mc-mobile-link">LEDGER</a>
|
||||||
<a href="/creative/ui" class="mc-mobile-link">CREATIVE</a>
|
<a href="/creative/ui" class="mc-mobile-link">CREATIVE</a>
|
||||||
<a href="/voice/button" class="mc-mobile-link">VOICE</a>
|
<a href="/voice/button" class="mc-mobile-link">VOICE</a>
|
||||||
<a href="/mobile" class="mc-mobile-link">MOBILE</a>
|
<a href="/mobile" class="mc-mobile-link">MOBILE</a>
|
||||||
@@ -128,6 +139,25 @@
|
|||||||
a.classList.add('active');
|
a.classList.add('active');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Desktop "More" dropdown toggle
|
||||||
|
var dropdownToggle = document.querySelector('.mc-dropdown-toggle');
|
||||||
|
if (dropdownToggle) {
|
||||||
|
dropdownToggle.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
var dd = this.closest('.mc-nav-dropdown');
|
||||||
|
var isOpen = dd.classList.toggle('open');
|
||||||
|
this.setAttribute('aria-expanded', isOpen);
|
||||||
|
});
|
||||||
|
document.addEventListener('click', function() {
|
||||||
|
var dd = document.querySelector('.mc-nav-dropdown');
|
||||||
|
if (dd) {
|
||||||
|
dd.classList.remove('open');
|
||||||
|
var btn = dd.querySelector('.mc-dropdown-toggle');
|
||||||
|
if (btn) btn.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/notifications.js"></script>
|
<script src="/static/notifications.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<!-- Agents (HTMX-polled from registry) -->
|
<!-- Agents (HTMX-polled from registry) -->
|
||||||
<div class="card mc-panel"
|
<div class="card mc-panel"
|
||||||
hx-get="/swarm/agents/sidebar"
|
hx-get="/swarm/agents/sidebar"
|
||||||
hx-trigger="load, every 10s"
|
hx-trigger="every 10s"
|
||||||
hx-target="this"
|
hx-target="this"
|
||||||
hx-swap="innerHTML">
|
hx-swap="innerHTML">
|
||||||
<div class="card-header mc-panel-header">// AGENTS</div>
|
<div class="card-header mc-panel-header">// AGENTS</div>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<!-- System Health (HTMX polled) -->
|
<!-- System Health (HTMX polled) -->
|
||||||
<div class="card mc-panel"
|
<div class="card mc-panel"
|
||||||
hx-get="/health/status"
|
hx-get="/health/status"
|
||||||
hx-trigger="load, every 30s"
|
hx-trigger="every 30s"
|
||||||
hx-target="this"
|
hx-target="this"
|
||||||
hx-swap="innerHTML">
|
hx-swap="innerHTML">
|
||||||
<div class="card-header mc-panel-header">// SYSTEM HEALTH</div>
|
<div class="card-header mc-panel-header">// SYSTEM HEALTH</div>
|
||||||
|
|||||||
@@ -23,3 +23,4 @@
|
|||||||
<div class="msg-body">Mission Control initialized. Timmy ready — awaiting input.</div>
|
<div class="msg-body">Mission Control initialized. Timmy ready — awaiting input.</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<script>if(typeof scrollChat==='function'){setTimeout(scrollChat,50);}</script>
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ from config import settings
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Project root — two levels up from src/self_modify/
|
# Project root — use settings.repo_root (works in Docker and local dev)
|
||||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
PROJECT_ROOT = Path(settings.repo_root)
|
||||||
|
|
||||||
# Reports directory
|
# Reports directory
|
||||||
REPORTS_DIR = PROJECT_ROOT / "data" / "self_modify_reports"
|
REPORTS_DIR = PROJECT_ROOT / "data" / "self_modify_reports"
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ class GrokBackend:
|
|||||||
) -> None:
|
) -> None:
|
||||||
from config import settings
|
from config import settings
|
||||||
|
|
||||||
self._api_key = api_key or settings.xai_api_key
|
self._api_key = api_key if api_key is not None else settings.xai_api_key
|
||||||
self._model = model or settings.grok_default_model
|
self._model = model or settings.grok_default_model
|
||||||
self._history: list[dict[str, str]] = []
|
self._history: list[dict[str, str]] = []
|
||||||
self.stats = GrokUsageStats()
|
self.stats = GrokUsageStats()
|
||||||
|
|||||||
@@ -145,6 +145,61 @@ a:hover { color: var(--orange); }
|
|||||||
}
|
}
|
||||||
.mc-test-link:hover { border-color: var(--purple); color: var(--purple); }
|
.mc-test-link:hover { border-color: var(--purple); color: var(--purple); }
|
||||||
|
|
||||||
|
/* ── Named link colors ───────────────────────────── */
|
||||||
|
.mc-link-thinking { color: #c084fc; }
|
||||||
|
.mc-link-bugs { color: #ff6b6b; }
|
||||||
|
.mc-link-grok { color: #00ff88; }
|
||||||
|
|
||||||
|
/* ── Desktop "More" dropdown ─────────────────────── */
|
||||||
|
.mc-nav-dropdown { position: relative; }
|
||||||
|
.mc-dropdown-toggle {
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.mc-dropdown-menu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
right: 0;
|
||||||
|
background: rgba(17, 8, 32, 0.96);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
-webkit-backdrop-filter: blur(24px);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 0;
|
||||||
|
min-width: 160px;
|
||||||
|
z-index: 150;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.mc-nav-dropdown:hover .mc-dropdown-menu,
|
||||||
|
.mc-nav-dropdown.open .mc-dropdown-menu {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.mc-dropdown-menu .mc-test-link {
|
||||||
|
display: block;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.mc-dropdown-menu .mc-test-link:hover {
|
||||||
|
background: rgba(124, 58, 237, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Mobile section labels ───────────────────────── */
|
||||||
|
.mc-mobile-section-label {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-dim);
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
padding: 12px 20px 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Hamburger (mobile only) ───────────────────── */
|
/* ── Hamburger (mobile only) ───────────────────── */
|
||||||
.mc-hamburger {
|
.mc-hamburger {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ def test_get_system_info_contains_repo_root():
|
|||||||
|
|
||||||
assert "repo_root" in info
|
assert "repo_root" in info
|
||||||
assert info["repo_root"] == settings.repo_root
|
assert info["repo_root"] == settings.repo_root
|
||||||
assert "Timmy-time-dashboard" in info["repo_root"]
|
# In Docker the CWD is /app, so just verify it's a non-empty path
|
||||||
|
assert len(info["repo_root"]) > 0
|
||||||
|
|
||||||
|
|
||||||
def test_check_ollama_health_returns_dict():
|
def test_check_ollama_health_returns_dict():
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ def test_resolve_path_relative_to_repo():
|
|||||||
|
|
||||||
result = _resolve_path("src/config.py")
|
result = _resolve_path("src/config.py")
|
||||||
|
|
||||||
assert "Timmy-time-dashboard" in str(result)
|
# In Docker the repo root is /app; locally it contains Timmy-time-dashboard
|
||||||
assert result.name == "config.py"
|
assert result.name == "config.py"
|
||||||
|
assert "src" in str(result)
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_path_absolute():
|
def test_resolve_path_absolute():
|
||||||
|
|||||||
Reference in New Issue
Block a user