forked from Rockachopa/Timmy-time-dashboard
Merge pull request #7 from Alexspayne/claude/quality-analysis-mobile-testing-0zgPi
This commit is contained in:
306
QUALITY_ANALYSIS.md
Normal file
306
QUALITY_ANALYSIS.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# Timmy Time — Senior Architect Quality Analysis
|
||||
**Date:** 2026-02-21
|
||||
**Branch:** `claude/quality-analysis-mobile-testing-0zgPi`
|
||||
**Test Suite:** 228/228 passing ✅
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Timmy Time has a strong Python backend skeleton and a working HTMX UI, but the project is at a **critical architectural fork**: a second, fully-detached React frontend was introduced that uses 100% mock/static data with zero API connectivity. This split creates the illusion of a richer app than exists. Completeness against the stated vision is **~35-40%**. The mobile HITL framework is the standout quality asset.
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture Coherence — CRITICAL ⚠️
|
||||
|
||||
**Score: 3/10**
|
||||
|
||||
### Finding: Dual Frontend, Zero Integration
|
||||
|
||||
The project ships two separate UIs that both claim to be "Mission Control":
|
||||
|
||||
| UI | Tech | Backend Connected? |
|
||||
|----|------|--------------------|
|
||||
| `src/dashboard/` | FastAPI + Jinja2 + HTMX | ✅ Yes — real Timmy chat, health, history |
|
||||
| `dashboard-web/` | React + TypeScript + Vite | ❌ No — 100% static mock data |
|
||||
|
||||
The React dashboard (`dashboard-web/client/src/lib/data.ts`) exports `MOCK_CHAT`, `MOCK_HEALTH`, `MOCK_NOTIFICATIONS`, `MOCK_TASKS`, `MOCK_WS_EVENTS` — every data source is hardcoded. There is **not a single `fetch()` call** to the FastAPI backend. The `ChatPanel` simulates responses with `setTimeout()`. The `StatusSidebar` shows a hardcoded Ollama status — it never calls `/health/status`.
|
||||
|
||||
**Impact:** The React UI is a clickable mockup, not a product. A new developer would not know which frontend is authoritative.
|
||||
|
||||
### Finding: React App Has No Build Config
|
||||
|
||||
`dashboard-web/client/` contains `src/` and `index.html` but no `package.json`, `vite.config.ts`, or `tsconfig.json` in that directory. The app imports from `@/components/ui/*` (shadcn/ui) but the `components/ui/` directory does not exist in the repo. The React app is **not buildable as committed**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Completeness Against Vision — 35-40%
|
||||
|
||||
**Score: 4/10**
|
||||
|
||||
| Feature | Roadmap | Status |
|
||||
|---------|---------|--------|
|
||||
| Agno + Ollama + SQLite dashboard | v1.0.0 | ✅ Complete |
|
||||
| HTMX chat with history | v1.0.0 | ✅ Complete |
|
||||
| AirLLM big-brain backend | v1.0.0 | ✅ Complete |
|
||||
| CLI (chat/think/status) | v1.0.0 | ✅ Complete |
|
||||
| Swarm registry + coordinator | v2.0.0 | ⚠️ Skeleton only — no real agents |
|
||||
| Agent personas (Echo, Mace, Forge…) | v2.0.0 | ❌ Catalog only — never instantiated |
|
||||
| MCP tools integration | v2.0.0 | ❌ Not started |
|
||||
| Voice NLU | v2.0.0 | ⚠️ Backend module — no live UI |
|
||||
| Push notifications | v2.0.0 | ⚠️ Backend module — never triggered |
|
||||
| Siri Shortcuts | v2.0.0 | ⚠️ Endpoint stub only |
|
||||
| WebSocket live swarm feed | v2.0.0 | ⚠️ Server-side ready — no UI consumer |
|
||||
| L402 / Lightning payments | v3.0.0 | ⚠️ Mock implementation only |
|
||||
| Real LND gRPC backend | v3.0.0 | ❌ Not started |
|
||||
| Single `.app` bundle | v3.0.0 | ❌ Not started |
|
||||
| React dashboard (live data) | — | ❌ All mock data |
|
||||
| Mobile HITL checklist | — | ✅ Complete (27 scenarios) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Mobile UX Audit
|
||||
|
||||
**Score: 7/10 (HTMX UI) / 2/10 (React UI)**
|
||||
|
||||
### HTMX Dashboard — Strong
|
||||
|
||||
The HTMX-served dashboard has solid mobile foundations verified by the automated test suite:
|
||||
|
||||
- ✅ `viewport-fit=cover` — Dynamic Island / notch support
|
||||
- ✅ `apple-mobile-web-app-capable` — Home Screen PWA mode
|
||||
- ✅ `safe-area-inset-top/bottom` — padding clears notch and home indicator
|
||||
- ✅ `overscroll-behavior: none` — no rubber-band on main page
|
||||
- ✅ `-webkit-overflow-scrolling: touch` — momentum scroll in chat
|
||||
- ✅ `dvh` units — correct height on iOS with collapsing chrome
|
||||
- ✅ 44px touch targets on SEND button and inputs
|
||||
- ✅ `font-size: 16px` in mobile query — iOS zoom prevention
|
||||
- ✅ `enterkeyhint="send"` — Send-labelled keyboard key
|
||||
- ✅ HTMX `hx-sync="this:drop"` — double-tap protection
|
||||
- ✅ HTMX `hx-disabled-elt` — in-flight button lockout
|
||||
|
||||
### Gap: Mobile Quick Actions Page (`/mobile`)
|
||||
|
||||
The `/mobile` route template shows a "Mobile only" page with quick action tiles and a JS-based chat — but it uses **CSS `display: none` on desktop** via `.mobile-only` with an `@media (min-width: 769px)` rule. The desktop fallback shows a placeholder. This is a valid progressive enhancement approach but the page is not linked from the main nav bar.
|
||||
|
||||
### React Dashboard — Mobile Not Functional
|
||||
|
||||
The React dashboard uses `hidden lg:flex` for the left sidebar (desktop only) and an `AnimatePresence` slide-in overlay for mobile. The mobile UX architecture is correct. However, because all data is mock, tapping "Chat" produces a simulated response from a setTimeout, not from Ollama. This is not tested and not usable.
|
||||
|
||||
---
|
||||
|
||||
## 4. Human-in-the-Loop (HITL) Mobile Testing
|
||||
|
||||
**Score: 8/10**
|
||||
|
||||
The `/mobile-test` route is the standout quality feature. It provides:
|
||||
|
||||
- 21 structured test scenarios across 7 categories (Layout, Touch, Chat, Health, Scroll, Notch, Live UI)
|
||||
- PASS/FAIL/SKIP buttons with sessionStorage persistence across scroll
|
||||
- Live pass rate counter and progress bar
|
||||
- Accessible on any phone via local network URL
|
||||
- ← MISSION CONTROL back-link for easy navigation
|
||||
|
||||
**Gaps to improve:**
|
||||
- No server-side results storage — results lost when tab closes
|
||||
- No shareable/exportable report (screenshot required for handoff)
|
||||
- React dashboard has no equivalent HITL page
|
||||
- No automated Playwright/Selenium mobile tests that could catch regressions
|
||||
|
||||
---
|
||||
|
||||
## 5. Security Assessment
|
||||
|
||||
**Score: 5/10**
|
||||
|
||||
### XSS Vulnerability — `/mobile` template
|
||||
|
||||
`mobile.html` line ~85 uses raw `innerHTML` string interpolation with user-supplied message content:
|
||||
|
||||
```javascript
|
||||
// mobile.html — VULNERABLE
|
||||
chat.innerHTML += `
|
||||
<div class="chat-message user">
|
||||
<div>${message}</div> <!-- message is user input, not escaped -->
|
||||
</div>
|
||||
`;
|
||||
```
|
||||
|
||||
If a user types `<img src=x onerror=alert(1)>`, it executes. This is a stored XSS via `innerHTML`. Fix: use `document.createTextNode(message)` or escape HTML before insertion.
|
||||
|
||||
The `swarm_live.html` has the same pattern with WebSocket data:
|
||||
```javascript
|
||||
container.innerHTML = agents.map(agent => `...${agent.name}...`).join('');
|
||||
```
|
||||
If agent names contain `<script>` tags (or any HTML), this executes in context.
|
||||
|
||||
### Hardcoded Secrets
|
||||
|
||||
`l402_proxy.py`: `_MACAROON_SECRET = "timmy-macaroon-secret".encode()` (default)
|
||||
`payment_handler.py`: `_HMAC_SECRET = "timmy-sovereign-sats".encode()` (default)
|
||||
|
||||
Both fall back to env var reads which is correct, but the defaults should not be production-safe strings — they should be None with a startup assertion requiring them to be set.
|
||||
|
||||
### No Route Authentication
|
||||
|
||||
All `/swarm/spawn`, `/swarm/tasks`, `/marketplace`, `/agents/timmy/chat` endpoints have no auth guard. On a `--host 0.0.0.0` server, anyone on the local network can post tasks or clear chat history. Acceptable for v1 local-only use but must be documented and gated before LAN exposure.
|
||||
|
||||
---
|
||||
|
||||
## 6. Test Coverage
|
||||
|
||||
**Score: 7/10**
|
||||
|
||||
| Suite | Tests | Quality |
|
||||
|-------|-------|---------|
|
||||
| Agent unit | 13 | Good |
|
||||
| Backends | 14 | Good |
|
||||
| Mobile scenarios | 32 | Excellent — covers M1xx-M6xx categories |
|
||||
| Swarm | 29+10+16 | Good |
|
||||
| L402 proxy | 13 | Good |
|
||||
| Voice NLU | 15 | Good |
|
||||
| Dashboard routes | 18+18 | Good |
|
||||
| WebSocket | 3 | Thin — no reconnect or message-type tests |
|
||||
| React components | 0 | Missing entirely |
|
||||
| End-to-end (Playwright) | 0 | Missing |
|
||||
|
||||
**Key gaps:**
|
||||
1. No tests for the XSS vulnerabilities
|
||||
2. No tests for the `/mobile` quick-chat endpoint
|
||||
3. WebSocket tests don't cover reconnection logic or malformed payloads
|
||||
4. React app has zero test coverage
|
||||
|
||||
---
|
||||
|
||||
## 7. Code Quality
|
||||
|
||||
**Score: 7/10**
|
||||
|
||||
**Strengths:**
|
||||
- Clean module separation (`timmy/`, `swarm/`, `dashboard/routes/`, `timmy_serve/`)
|
||||
- Consistent use of dataclasses for domain models
|
||||
- Good docstrings on all public functions
|
||||
- SQLite-backed persistence for both Agno memory and swarm registry
|
||||
- pydantic-settings config with `.env` override support
|
||||
|
||||
**Weaknesses:**
|
||||
- Swarm `coordinator.py` uses a module-level singleton `coordinator = SwarmCoordinator()` — not injectable, hard to test in isolation
|
||||
- `swarm/registry.py` opens a new SQLite connection on every call (no connection pool)
|
||||
- `dashboard/routes/swarm.py` creates a new `Jinja2Templates` instance — it should reuse the one from `app.py`
|
||||
- React components import from `@/components/ui/*` which don't exist in the committed tree
|
||||
|
||||
---
|
||||
|
||||
## 8. Developer Experience
|
||||
|
||||
**Score: 6/10**
|
||||
|
||||
**Strengths:**
|
||||
- README is excellent — copy-paste friendly, covers Mac quickstart, phone access, troubleshooting
|
||||
- DEVELOPMENT_REPORT.md provides full history of what was built and why
|
||||
- `.env.example` covers all config variables
|
||||
- Self-TDD watchdog CLI is a creative addition
|
||||
|
||||
**Weaknesses:**
|
||||
- No `docker-compose.yml` — setup requires manual Python venv + Ollama install
|
||||
- Two apps (FastAPI + React) with no single `make dev` command to start both
|
||||
- `STATUS.md` says v1.0.0 but development is well past that — version drift
|
||||
- React app missing from the quickstart instructions entirely
|
||||
|
||||
---
|
||||
|
||||
## 9. Backend Architecture
|
||||
|
||||
**Score: 7/10**
|
||||
|
||||
The FastAPI backend is well-structured. The swarm subsystem follows a clean coordinator pattern. The L402 mock is architecturally correct (the interface matches what real LND calls would require).
|
||||
|
||||
**Gaps:**
|
||||
- Swarm "agents" are database records — `spawn_agent()` registers a record but no Python process is actually launched. `agent_runner.py` uses `subprocess.Popen` to run `python -m swarm.agent_runner` but no `__main__` block exists in that file.
|
||||
- The bidding system (`bidder.py`) runs an asyncio auction but there are no actual bidder agents submitting bids — auctions will always time out with no winner.
|
||||
- Voice TTS (`voice_tts.py`) requires `pyttsx3` (optional dep) but the voice route offers no graceful fallback message when pyttsx3 is absent.
|
||||
|
||||
---
|
||||
|
||||
## 10. Prioritized Defects
|
||||
|
||||
| Priority | ID | Issue | File |
|
||||
|----------|----|-------|------|
|
||||
| P0 | SEC-01 | XSS via innerHTML with unsanitized user input | `mobile.html:85`, `swarm_live.html:72` |
|
||||
| P0 | ARCH-01 | React dashboard 100% mock — no backend calls | `dashboard-web/client/src/` |
|
||||
| P0 | ARCH-02 | React app not buildable — missing package.json, shadcn/ui | `dashboard-web/client/` |
|
||||
| P1 | SEC-02 | Hardcoded L402/HMAC secrets without startup assertion | `l402_proxy.py`, `payment_handler.py` |
|
||||
| P1 | FUNC-01 | Swarm spawn creates DB record but no process | `swarm/agent_runner.py` |
|
||||
| P1 | FUNC-02 | Auction always fails — no real bid submitters | `swarm/bidder.py` |
|
||||
| P2 | UX-01 | `/mobile` route not in desktop nav | `base.html`, `index.html` |
|
||||
| P2 | TEST-01 | WebSocket reconnection not tested | `tests/test_websocket.py` |
|
||||
| P2 | DX-01 | No single dev startup command | `README.md` |
|
||||
| P3 | PERF-01 | SQLite connection opened per-query in registry | `swarm/registry.py` |
|
||||
|
||||
---
|
||||
|
||||
## HITL Mobile Test Session Guide
|
||||
|
||||
To run a complete human-in-the-loop mobile test session right now:
|
||||
|
||||
```bash
|
||||
# 1. Start the dashboard
|
||||
source .venv/bin/activate
|
||||
uvicorn dashboard.app:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
# 2. Find your local IP
|
||||
ipconfig getifaddr en0 # macOS
|
||||
hostname -I # Linux
|
||||
|
||||
# 3. Open on your phone (same Wi-Fi)
|
||||
http://<YOUR_IP>:8000/mobile-test
|
||||
|
||||
# 4. Work through the 21 scenarios, marking PASS / FAIL / SKIP
|
||||
# 5. Screenshot the SUMMARY section for your records
|
||||
|
||||
# ─── Also test the main dashboard on mobile ───────────────────────
|
||||
http://<YOUR_IP>:8000 # Main Mission Control
|
||||
http://<YOUR_IP>:8000/mobile # Quick Actions (mobile-optimized)
|
||||
```
|
||||
|
||||
**Critical scenarios to test first:**
|
||||
- T01 — iOS zoom prevention (tap input, watch for zoom)
|
||||
- C02 — Multi-turn memory (tell Timmy your name, ask it back)
|
||||
- C04 — Offline graceful error (stop Ollama, send message)
|
||||
- N01/N02 — Notch / home bar clearance (notched iPhone)
|
||||
|
||||
---
|
||||
|
||||
## Recommended Next Prompt for Development
|
||||
|
||||
```
|
||||
Connect the React dashboard (dashboard-web/) to the live FastAPI backend.
|
||||
|
||||
Priority order:
|
||||
|
||||
1. FIX BUILD FIRST: Add package.json, vite.config.ts, tailwind.config.ts, and
|
||||
tsconfig.json to dashboard-web/client/. Install shadcn/ui so the existing
|
||||
component imports resolve. Verify `npm run dev` starts the app.
|
||||
|
||||
2. CHAT (highest user value): Replace ChatPanel mock with real fetch to
|
||||
POST /agents/timmy/chat. Show the actual Timmy response from Ollama.
|
||||
Implement loading state (matches the existing isTyping UI).
|
||||
|
||||
3. HEALTH: Replace MOCK_HEALTH in StatusSidebar with a polling fetch to
|
||||
GET /health/status (every 30s, matching HTMX behaviour).
|
||||
|
||||
4. SWARM WEBSOCKET: Open a real WebSocket to ws://localhost:8000/swarm/ws
|
||||
and pipe state updates into SwarmPanel — replacing MOCK_WS_EVENTS.
|
||||
|
||||
5. SECURITY: Fix XSS in mobile.html and swarm_live.html — replace innerHTML
|
||||
string interpolation with safe DOM methods (textContent / createTextNode).
|
||||
|
||||
Use React Query (TanStack) for data fetching with stale-while-revalidate.
|
||||
Keep the existing HTMX dashboard running in parallel — the React app should
|
||||
be the forward-looking UI.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Analysis by Claude Code — Senior Architect Review*
|
||||
*Timmy Time Dashboard | branch: claude/quality-analysis-mobile-testing-0zgPi*
|
||||
@@ -1,24 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1" />
|
||||
<title>Timmy Time — Mission Control</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Fira+Code:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script
|
||||
defer
|
||||
src="%VITE_ANALYTICS_ENDPOINT%/umami"
|
||||
data-website-id="%VITE_ANALYTICS_WEBSITE_ID%"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import NotFound from "@/pages/NotFound";
|
||||
import { Route, Switch } from "wouter";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
|
||||
function Router() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/" component={Dashboard} />
|
||||
<Route path="/404" component={NotFound} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ThemeProvider defaultTheme="dark">
|
||||
<TooltipProvider>
|
||||
<Toaster
|
||||
position="top-right"
|
||||
toastOptions={{
|
||||
style: {
|
||||
background: 'oklch(0.12 0.01 260)',
|
||||
border: '1px solid oklch(0.22 0.01 260)',
|
||||
borderTop: '2px solid oklch(0.75 0.18 55)',
|
||||
color: 'oklch(0.9 0.005 90)',
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontSize: '12px',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Router />
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,186 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Chat interface
|
||||
* Terminal-style command input with >_ prompt cursor
|
||||
* Messages displayed with typewriter aesthetic, typing indicator
|
||||
*/
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Send } from "lucide-react";
|
||||
import { MOCK_CHAT, type ChatMessage } from "@/lib/data";
|
||||
import { motion } from "framer-motion";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function ChatPanel() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>(MOCK_CHAT);
|
||||
const [input, setInput] = useState("");
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages, isTyping]);
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim()) return;
|
||||
|
||||
// Handle slash commands
|
||||
if (input.startsWith("/")) {
|
||||
const cmd = input.slice(1).trim().split(" ")[0];
|
||||
toast(`Command recognized: /${cmd}`, {
|
||||
description: "Slash commands will be processed when connected to the backend.",
|
||||
});
|
||||
setInput("");
|
||||
return;
|
||||
}
|
||||
|
||||
const userMsg: ChatMessage = {
|
||||
id: `c-${Date.now()}`,
|
||||
role: "user",
|
||||
content: input,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMsg]);
|
||||
setInput("");
|
||||
setIsTyping(true);
|
||||
|
||||
// Simulate response with typing delay
|
||||
setTimeout(() => {
|
||||
setIsTyping(false);
|
||||
const assistantMsg: ChatMessage = {
|
||||
id: `c-${Date.now() + 1}`,
|
||||
role: "assistant",
|
||||
content: "I hear you, boss. Running locally on Ollama — no cloud, no telemetry. Your sovereignty is intact.\n\nThis is a demo interface. Connect me to your local Ollama instance to get real responses.\n\nSats are sovereignty, boss.",
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
setMessages(prev => [...prev, assistantMsg]);
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Messages area */}
|
||||
<div ref={scrollRef} className="flex-1 overflow-auto p-4 space-y-4">
|
||||
{messages.map((msg, i) => (
|
||||
<motion.div
|
||||
key={msg.id}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: i < 4 ? i * 0.05 : 0 }}
|
||||
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div className={`max-w-[80%] ${msg.role === 'system' ? 'w-full' : ''}`}>
|
||||
{/* Role label */}
|
||||
<div className={`text-[10px] uppercase tracking-[0.15em] mb-1 ${
|
||||
msg.role === 'user' ? 'text-right text-muted-foreground' :
|
||||
msg.role === 'system' ? 'text-cyber-cyan' :
|
||||
'text-btc-orange'
|
||||
}`}>
|
||||
{msg.role === 'assistant' ? '// TIMMY' :
|
||||
msg.role === 'system' ? '// SYSTEM' :
|
||||
'// YOU'}
|
||||
</div>
|
||||
|
||||
{/* Message bubble */}
|
||||
<div className={`text-[13px] leading-relaxed ${
|
||||
msg.role === 'user'
|
||||
? 'bg-accent border border-border px-4 py-3 text-foreground'
|
||||
: msg.role === 'system'
|
||||
? 'bg-cyber-cyan/5 border border-cyber-cyan/20 px-4 py-3 text-cyber-cyan/80 font-mono text-[11px]'
|
||||
: 'bg-card border border-border px-4 py-3 text-foreground'
|
||||
}`}
|
||||
style={msg.role === 'assistant' ? { borderTop: '2px solid oklch(0.75 0.18 55)' } : undefined}
|
||||
>
|
||||
{msg.content.split('\n').map((line, j) => (
|
||||
<p key={j} className={j > 0 ? 'mt-2' : ''}>
|
||||
{line || '\u00A0'}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Timestamp */}
|
||||
<div className={`text-[9px] text-muted-foreground mt-1 ${
|
||||
msg.role === 'user' ? 'text-right' : ''
|
||||
}`}>
|
||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{/* Typing indicator */}
|
||||
{isTyping && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="flex justify-start"
|
||||
>
|
||||
<div>
|
||||
<div className="text-[10px] uppercase tracking-[0.15em] mb-1 text-btc-orange">
|
||||
// TIMMY
|
||||
</div>
|
||||
<div className="bg-card border border-border px-4 py-3 text-foreground"
|
||||
style={{ borderTop: '2px solid oklch(0.75 0.18 55)' }}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-btc-orange animate-pulse" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-btc-orange animate-pulse" style={{ animationDelay: '0.2s' }} />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-btc-orange animate-pulse" style={{ animationDelay: '0.4s' }} />
|
||||
<span className="text-[11px] text-muted-foreground ml-2">thinking...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input area */}
|
||||
<div className="border-t border-border p-3">
|
||||
<div
|
||||
className="flex items-center gap-2 bg-input border border-border px-3 py-2.5 focus-within:border-btc-orange/50 transition-colors"
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
>
|
||||
<span className="terminal-prompt text-[13px] select-none">>_</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type a message or /command..."
|
||||
className="flex-1 bg-transparent text-[13px] text-foreground placeholder:text-muted-foreground outline-none"
|
||||
style={{ fontSize: '16px' }}
|
||||
disabled={isTyping}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-btc-orange hover:text-btc-orange/80 hover:bg-btc-orange/10"
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim() || isTyping}
|
||||
>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-1.5 text-[9px] text-muted-foreground">
|
||||
<span>ENTER to send</span>
|
||||
<span className="text-border">|</span>
|
||||
<span>/help for commands</span>
|
||||
<span className="text-border">|</span>
|
||||
<span className="text-electric-green/60">Local LLM — no cloud</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Right context panel
|
||||
* Shows details for selected agent or task, voice NLU, or roadmap
|
||||
*/
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
AGENT_CATALOG, MOCK_TASKS, VOICE_INTENTS, ROADMAP,
|
||||
type Agent, type Task
|
||||
} from "@/lib/data";
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
User, Zap, Mic, Map, ChevronRight,
|
||||
CheckCircle, Clock, AlertCircle, Loader2, Gavel
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const HERO_IMG = "https://private-us-east-1.manuscdn.com/sessionFile/hmEvCGQLHKyGnx6qwMSEHn/sandbox/qiXHjJUmj8lqJymwhLI5B2-img-1_1771695716000_na1fn_aGVyby1iYW5uZXI.png?x-oss-process=image/resize,w_1920,h_1920/format,webp/quality,q_80&Expires=1798761600&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9wcml2YXRlLXVzLWVhc3QtMS5tYW51c2Nkbi5jb20vc2Vzc2lvbkZpbGUvaG1FdkNHUUxIS3lHbng2cXdNU0VIbi9zYW5kYm94L3FpWEhqSlVtajhscUp5bXdoTEk1QjItaW1nLTFfMTc3MTY5NTcxNjAwMF9uYTFmbl9hR1Z5YnkxaVlXNXVaWEkucG5nP3gtb3NzLXByb2Nlc3M9aW1hZ2UvcmVzaXplLHdfMTkyMCxoXzE5MjAvZm9ybWF0LHdlYnAvcXVhbGl0eSxxXzgwIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNzk4NzYxNjAwfX19XX0_&Key-Pair-Id=K2HSFNDJXOU9YS&Signature=Yeq1vIhtaEw73bIbDJlFsrAQeoce-YaWvid7nAdYEKAA41Xxzh8iioV-HmHsbldg~z674zIlRc0KeBIdV2hH2O8yBRN6KjP-BMO9QHDbGeBbTw3Bd5uEbh7GmZUXb7klkd0yStYYQcIjwTPBcJ7dMkiQ4AV1k5u63gQDm1FS-hqRGqzcS97ZQc0eSd3Ij2CKLrF7OXc4Xu6wB8CxzLD87mTdnvOtLobjHgvFdl6KVkUTIHjh97fL8YRlN5My6N3BGW-E8l-ZNVnWT22qfiHcpVD4kk6S6yu~v7OpBY3-1if3am5B2prST3bHxGMKsQlTwttr~xEpX4ZYF1dAJy0n2Q__";
|
||||
|
||||
interface ContextPanelProps {
|
||||
selectedAgent: string | null;
|
||||
selectedTask: string | null;
|
||||
}
|
||||
|
||||
export default function ContextPanel({ selectedAgent, selectedTask }: ContextPanelProps) {
|
||||
const agent = selectedAgent ? AGENT_CATALOG.find(a => a.id === selectedAgent) : null;
|
||||
const task = selectedTask ? MOCK_TASKS.find(t => t.id === selectedTask) : null;
|
||||
|
||||
return (
|
||||
<div className="p-3 space-y-3">
|
||||
{/* Agent detail */}
|
||||
{agent && <AgentDetail agent={agent} />}
|
||||
|
||||
{/* Task detail */}
|
||||
{task && <TaskDetail task={task} />}
|
||||
|
||||
{/* Voice NLU */}
|
||||
<VoiceNLUPanel />
|
||||
|
||||
{/* Roadmap */}
|
||||
<RoadmapPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentDetail({ agent }: { agent: Agent }) {
|
||||
return (
|
||||
<motion.div
|
||||
key={agent.id}
|
||||
initial={{ opacity: 0, x: 16 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="panel"
|
||||
>
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<User className="w-3 h-3" />
|
||||
// AGENT DETAIL
|
||||
</div>
|
||||
<div className="p-3 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`status-dot ${
|
||||
agent.status === 'active' ? 'status-dot-active' : 'status-dot-planned'
|
||||
}`} />
|
||||
<span className="text-[14px] font-semibold text-foreground">{agent.name}</span>
|
||||
<Badge variant="outline" className="text-[9px] h-4 ml-auto">
|
||||
{agent.status.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="text-[11px] text-btc-orange font-medium">{agent.role}</div>
|
||||
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||
{agent.description}
|
||||
</p>
|
||||
|
||||
<Separator className="bg-border" />
|
||||
|
||||
<div className="space-y-2 text-[11px]">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Rate</span>
|
||||
{agent.rateSats === 0 ? (
|
||||
<span className="text-electric-green">FREE</span>
|
||||
) : (
|
||||
<span className="sat-amount flex items-center gap-1">
|
||||
<Zap className="w-3 h-3" />
|
||||
{agent.rateSats} sats/task
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Capabilities</span>
|
||||
<span className="text-foreground">{agent.capabilities.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{agent.capabilities.map(cap => (
|
||||
<span key={cap} className="text-[9px] px-1.5 py-0.5 bg-accent text-accent-foreground border border-border">
|
||||
{cap}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{agent.status === 'active' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full h-7 text-[10px] text-btc-orange hover:bg-btc-orange/10 border border-btc-orange/30"
|
||||
onClick={() => toast("Chat with " + agent.name, { description: "Switch to the Chat tab to interact." })}
|
||||
>
|
||||
OPEN CHAT
|
||||
<ChevronRight className="w-3 h-3 ml-1" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskDetail({ task }: { task: Task }) {
|
||||
return (
|
||||
<motion.div
|
||||
key={task.id}
|
||||
initial={{ opacity: 0, x: 16 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="panel"
|
||||
>
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<Clock className="w-3 h-3" />
|
||||
// TASK DETAIL
|
||||
</div>
|
||||
<div className="p-3 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] text-muted-foreground font-mono">{task.id}</span>
|
||||
<TaskStatusBadge status={task.status} />
|
||||
</div>
|
||||
|
||||
<p className="text-[12px] text-foreground leading-relaxed">
|
||||
{task.description}
|
||||
</p>
|
||||
|
||||
<Separator className="bg-border" />
|
||||
|
||||
<div className="space-y-2 text-[11px]">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Assigned</span>
|
||||
<span className={task.assignedAgent ? 'text-btc-orange' : 'text-muted-foreground'}>
|
||||
{task.assignedAgent || 'Unassigned'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Created</span>
|
||||
<span className="text-foreground">
|
||||
{new Date(task.createdAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
{task.completedAt && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Completed</span>
|
||||
<span className="text-electric-green">
|
||||
{new Date(task.completedAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{task.result && (
|
||||
<>
|
||||
<Separator className="bg-border" />
|
||||
<div>
|
||||
<div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1">Result</div>
|
||||
<p className="text-[11px] text-foreground bg-accent p-2 border border-border">
|
||||
{task.result}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskStatusBadge({ status }: { status: Task["status"] }) {
|
||||
const colors: Record<Task["status"], string> = {
|
||||
pending: "bg-muted text-muted-foreground",
|
||||
bidding: "bg-warning-amber/20 text-warning-amber border-warning-amber/30",
|
||||
assigned: "bg-cyber-cyan/20 text-cyber-cyan border-cyber-cyan/30",
|
||||
running: "bg-btc-orange/20 text-btc-orange border-btc-orange/30",
|
||||
completed: "bg-electric-green/20 text-electric-green border-electric-green/30",
|
||||
failed: "bg-danger-red/20 text-danger-red border-danger-red/30",
|
||||
};
|
||||
return (
|
||||
<span className={`text-[9px] uppercase tracking-wider px-1.5 py-0.5 border ${colors[status]}`}>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function VoiceNLUPanel() {
|
||||
return (
|
||||
<div className="panel">
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<Mic className="w-3 h-3" />
|
||||
// VOICE NLU
|
||||
</div>
|
||||
<div className="p-3 space-y-2">
|
||||
<p className="text-[10px] text-muted-foreground mb-2">
|
||||
Supported voice intents — local regex-based NLU, no cloud.
|
||||
</p>
|
||||
{VOICE_INTENTS.map(intent => (
|
||||
<div key={intent.name} className="flex items-start gap-2 text-[11px]">
|
||||
<span className="text-btc-orange font-medium w-14 flex-shrink-0 uppercase text-[10px]">
|
||||
{intent.name}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-muted-foreground">{intent.description}</span>
|
||||
<div className="text-[9px] text-muted-foreground/60 font-mono mt-0.5 truncate">
|
||||
"{intent.example}"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RoadmapPanel() {
|
||||
return (
|
||||
<div className="panel">
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<Map className="w-3 h-3" />
|
||||
// ROADMAP
|
||||
</div>
|
||||
<div className="p-3 space-y-2">
|
||||
{ROADMAP.map((item, i) => (
|
||||
<div key={item.version} className="flex items-start gap-2 text-[11px]">
|
||||
<div className="flex flex-col items-center mt-0.5">
|
||||
<span className={`w-2 h-2 rounded-full ${
|
||||
item.status === 'complete' ? 'bg-electric-green' :
|
||||
item.status === 'current' ? 'bg-btc-orange' : 'bg-muted-foreground'
|
||||
}`} />
|
||||
{i < ROADMAP.length - 1 && (
|
||||
<span className="w-px h-6 bg-border mt-1" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-foreground">{item.version}</span>
|
||||
<span className="text-btc-orange">{item.name}</span>
|
||||
{item.status === 'current' && (
|
||||
<span className="text-[8px] px-1 py-0.5 bg-btc-orange/20 text-btc-orange border border-btc-orange/30">
|
||||
CURRENT
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-muted-foreground text-[10px]">{item.milestone}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Agent Marketplace
|
||||
* Browse agents, see capabilities, hire with sats
|
||||
* Lightning payment visualization as hero
|
||||
*/
|
||||
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AGENT_CATALOG, type Agent } from "@/lib/data";
|
||||
import { motion } from "framer-motion";
|
||||
import { Store, Zap, ChevronRight } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const LIGHTNING_IMG = "https://private-us-east-1.manuscdn.com/sessionFile/hmEvCGQLHKyGnx6qwMSEHn/sandbox/qiXHjJUmj8lqJymwhLI5B2-img-3_1771695706000_na1fn_bGlnaHRuaW5nLXBheW1lbnQ.png?x-oss-process=image/resize,w_1920,h_1920/format,webp/quality,q_80&Expires=1798761600&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9wcml2YXRlLXVzLWVhc3QtMS5tYW51c2Nkbi5jb20vc2Vzc2lvbkZpbGUvaG1FdkNHUUxIS3lHbng2cXdNU0VIbi9zYW5kYm94L3FpWEhqSlVtajhscUp5bXdoTEk1QjItaW1nLTNfMTc3MTY5NTcwNjAwMF9uYTFmbl9iR2xuYUhSdWFXNW5MWEJoZVcxbGJuUS5wbmc~eC1vc3MtcHJvY2Vzcz1pbWFnZS9yZXNpemUsd18xOTIwLGhfMTkyMC9mb3JtYXQsd2VicC9xdWFsaXR5LHFfODAiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE3OTg3NjE2MDB9fX1dfQ__&Key-Pair-Id=K2HSFNDJXOU9YS&Signature=gWuDcQJeJeaEupkqbE5tOSIOgB6A2LjDuU7w6nK8RzOSmeWWy~4AJVsm68hi--j22DFlv7hDWhZnoQ9WdyU0oCn3tIUFPaaamtcUY-9qBE3yw9VjAnBRJjG3ppnfVSFY-KaVvuX2hjkgzeknhsEmSuIo55yL6Y8c4CwsoVeLW7AloD9ou-2xBEKNObQqwRG~FP~cMMLOyNoPDzwclB8B~Imm3Qd~0-LAfKDp0nksbpBV87IN8YKsFxyAV5Bq~Mm-wqlGJZwBGzYfOPQQUNaTYZ2zzIidxTMNDLUE70fgc~oI2~0i2ebq-~8QFJwuLywTVycxV61BKssTsiOMBizE0g__";
|
||||
|
||||
interface MarketplacePanelProps {
|
||||
onSelectAgent: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function MarketplacePanel({ onSelectAgent }: MarketplacePanelProps) {
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-4">
|
||||
{/* Hero banner */}
|
||||
<div className="relative h-[200px] overflow-hidden mb-6 border border-border"
|
||||
style={{ borderTop: '2px solid oklch(0.75 0.18 55)' }}
|
||||
>
|
||||
<img
|
||||
src={LIGHTNING_IMG}
|
||||
alt="Lightning Network"
|
||||
className="w-full h-full object-cover opacity-60"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-background via-background/60 to-transparent" />
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4">
|
||||
<div className="text-[11px] uppercase tracking-[0.15em] text-btc-orange mb-1">
|
||||
// AGENT MARKETPLACE
|
||||
</div>
|
||||
<p className="text-[13px] text-foreground max-w-md leading-relaxed">
|
||||
Hire specialized agents with Lightning sats. Each agent bids on tasks
|
||||
through the L402 auction system. No API keys — just sats.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent catalog */}
|
||||
<div className="text-[11px] uppercase tracking-[0.15em] text-muted-foreground mb-3">
|
||||
// AVAILABLE AGENTS
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{AGENT_CATALOG.map((agent, i) => (
|
||||
<motion.div
|
||||
key={agent.id}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.06 }}
|
||||
className="panel p-4 hover:bg-panel-hover transition-colors group"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`status-dot ${
|
||||
agent.status === 'active' ? 'status-dot-active' : 'status-dot-planned'
|
||||
}`} />
|
||||
<span className="text-[14px] font-semibold text-foreground">{agent.name}</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-muted-foreground">{agent.role}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
{agent.rateSats === 0 ? (
|
||||
<Badge variant="outline" className="text-[9px] border-electric-green/30 text-electric-green">
|
||||
FREE
|
||||
</Badge>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<Zap className="w-3 h-3 text-btc-orange" />
|
||||
<span className="sat-amount text-[13px]">{agent.rateSats}</span>
|
||||
<span className="text-[9px] text-muted-foreground">sats/task</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed mb-3">
|
||||
{agent.description}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{agent.capabilities.map(cap => (
|
||||
<span key={cap} className="text-[9px] px-1.5 py-0.5 bg-accent text-accent-foreground border border-border">
|
||||
{cap}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-[9px] ${
|
||||
agent.status === 'active'
|
||||
? 'border-electric-green/30 text-electric-green'
|
||||
: 'border-muted-foreground/30 text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{agent.status.toUpperCase()}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 text-[10px] text-btc-orange hover:text-btc-orange/80 hover:bg-btc-orange/10 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={() => {
|
||||
if (agent.status === 'active') {
|
||||
onSelectAgent(agent.id);
|
||||
} else {
|
||||
toast("Agent not yet available", {
|
||||
description: `${agent.name} is planned for a future release.`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{agent.status === 'active' ? 'VIEW' : 'COMING SOON'}
|
||||
<ChevronRight className="w-3 h-3 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Left sidebar with stacked status panels
|
||||
* Each panel has a 2px Bitcoin orange top border and monospace headers
|
||||
*/
|
||||
|
||||
import { Activity, Bell, Zap, Users, ChevronRight } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
AGENT_CATALOG, MOCK_HEALTH, MOCK_NOTIFICATIONS,
|
||||
} from "@/lib/data";
|
||||
|
||||
interface StatusSidebarProps {
|
||||
onSelectAgent: (id: string) => void;
|
||||
selectedAgent: string | null;
|
||||
}
|
||||
|
||||
export default function StatusSidebar({ onSelectAgent, selectedAgent }: StatusSidebarProps) {
|
||||
return (
|
||||
<div className="p-3 space-y-3">
|
||||
{/* System Health Panel */}
|
||||
<div className="panel">
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<Activity className="w-3 h-3" />
|
||||
<span>// SYSTEM HEALTH</span>
|
||||
</div>
|
||||
<div className="p-3 text-[12px]">
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Ollama</td>
|
||||
<td className="text-right py-1">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className={`status-dot ${MOCK_HEALTH.ollama === 'up' ? 'status-dot-active' : 'status-dot-danger'}`} />
|
||||
<span className={MOCK_HEALTH.ollama === 'up' ? 'text-electric-green' : 'text-danger-red'}>
|
||||
{MOCK_HEALTH.ollama.toUpperCase()}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Model</td>
|
||||
<td className="text-right py-1 text-foreground">{MOCK_HEALTH.model}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Swarm</td>
|
||||
<td className="text-right py-1">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className={`status-dot ${MOCK_HEALTH.swarmRegistry === 'active' ? 'status-dot-active' : 'status-dot-warning'}`} />
|
||||
<span className={MOCK_HEALTH.swarmRegistry === 'active' ? 'text-electric-green' : 'text-warning-amber'}>
|
||||
{MOCK_HEALTH.swarmRegistry.toUpperCase()}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Uptime</td>
|
||||
<td className="text-right py-1 text-foreground">{MOCK_HEALTH.uptime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Tasks</td>
|
||||
<td className="text-right py-1 text-foreground">
|
||||
<span className="text-electric-green">{MOCK_HEALTH.completedTasks}</span>
|
||||
<span className="text-muted-foreground">/</span>
|
||||
<span>{MOCK_HEALTH.totalTasks}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agents Panel */}
|
||||
<div className="panel">
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<Users className="w-3 h-3" />
|
||||
<span>// AGENTS</span>
|
||||
</div>
|
||||
<div className="p-1">
|
||||
{AGENT_CATALOG.map((agent) => (
|
||||
<button
|
||||
key={agent.id}
|
||||
onClick={() => onSelectAgent(agent.id)}
|
||||
className={`w-full text-left px-3 py-2 flex items-center gap-2.5 text-[12px] transition-colors hover:bg-accent ${
|
||||
selectedAgent === agent.id ? 'bg-accent border-l-2 border-btc-orange' : ''
|
||||
}`}
|
||||
>
|
||||
<span className={`status-dot flex-shrink-0 ${
|
||||
agent.status === 'active' ? 'status-dot-active' :
|
||||
agent.status === 'planned' ? 'status-dot-planned' : 'status-dot-danger'
|
||||
}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-medium text-foreground">{agent.name}</span>
|
||||
{agent.rateSats > 0 && (
|
||||
<span className="text-[10px] text-btc-orange">{agent.rateSats} sats</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground truncate block">
|
||||
{agent.role}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronRight className="w-3 h-3 text-muted-foreground flex-shrink-0" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* L402 Balance Panel */}
|
||||
<div className="panel">
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<Zap className="w-3 h-3" />
|
||||
<span>// L402 TREASURY</span>
|
||||
</div>
|
||||
<div className="p-3 text-[12px]">
|
||||
<div className="text-center mb-3">
|
||||
<div className="sat-amount text-[20px] font-bold">
|
||||
₿ {MOCK_HEALTH.l402Balance.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground mt-0.5">satoshis available</div>
|
||||
</div>
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Protocol</td>
|
||||
<td className="text-right py-1 text-foreground">L402 / Lightning</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Macaroon</td>
|
||||
<td className="text-right py-1">
|
||||
<span className="text-electric-green text-[10px] uppercase tracking-wider">Valid</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Network</td>
|
||||
<td className="text-right py-1 text-foreground">Testnet</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notifications Panel */}
|
||||
<div className="panel">
|
||||
<div className="panel-header flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell className="w-3 h-3" />
|
||||
<span>// NOTIFICATIONS</span>
|
||||
</div>
|
||||
<Badge variant="secondary" className="text-[9px] h-4 px-1.5 bg-btc-orange/20 text-btc-orange border-0">
|
||||
{MOCK_NOTIFICATIONS.filter(n => !n.read).length} new
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="p-1">
|
||||
{MOCK_NOTIFICATIONS.slice(0, 4).map((notif) => (
|
||||
<div
|
||||
key={notif.id}
|
||||
className={`px-3 py-2 text-[11px] border-b border-border/50 last:border-0 ${
|
||||
!notif.read ? 'bg-accent/50' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 mb-0.5">
|
||||
{!notif.read && <span className="w-1.5 h-1.5 rounded-full bg-btc-orange flex-shrink-0" />}
|
||||
<span className="font-medium text-foreground truncate">{notif.title}</span>
|
||||
<span className="text-[9px] text-muted-foreground ml-auto flex-shrink-0">
|
||||
{getCategoryIcon(notif.category)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground truncate pl-3">{notif.message}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getCategoryIcon(category: string): string {
|
||||
switch (category) {
|
||||
case "swarm": return "⚡";
|
||||
case "task": return "📋";
|
||||
case "agent": return "🤖";
|
||||
case "system": return "⚙️";
|
||||
case "payment": return "₿";
|
||||
default: return "•";
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Swarm management panel
|
||||
* Shows agent constellation visualization and live event feed
|
||||
*/
|
||||
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { AGENT_CATALOG, MOCK_WS_EVENTS, type Agent } from "@/lib/data";
|
||||
import { motion } from "framer-motion";
|
||||
import { Network, Radio, Zap } from "lucide-react";
|
||||
|
||||
const SWARM_IMG = "https://private-us-east-1.manuscdn.com/sessionFile/hmEvCGQLHKyGnx6qwMSEHn/sandbox/qiXHjJUmj8lqJymwhLI5B2-img-2_1771695716000_na1fn_c3dhcm0tbmV0d29yaw.png?x-oss-process=image/resize,w_1920,h_1920/format,webp/quality,q_80&Expires=1798761600&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9wcml2YXRlLXVzLWVhc3QtMS5tYW51c2Nkbi5jb20vc2Vzc2lvbkZpbGUvaG1FdkNHUUxIS3lHbng2cXdNU0VIbi9zYW5kYm94L3FpWEhqSlVtajhscUp5bXdoTEk1QjItaW1nLTJfMTc3MTY5NTcxNjAwMF9uYTFmbl9jM2RoY20wdGJtVjBkMjl5YXcucG5nP3gtb3NzLXByb2Nlc3M9aW1hZ2UvcmVzaXplLHdfMTkyMCxoXzE5MjAvZm9ybWF0LHdlYnAvcXVhbGl0eSxxXzgwIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNzk4NzYxNjAwfX19XX0_&Key-Pair-Id=K2HSFNDJXOU9YS&Signature=rJ6lQ-h3pSQDDcUkGSTmXY2409jDYW2LdC9FU2ifVTnfppMXRrupq2SRC4e5P~Q5zx2r1ckGCWAi954bOr62u43lAXcxXn-FbW7PPVhoh3hx2LqGQrPLbSNbMw0-2AYO~4iKbUa~7igW2XdxeErPWs-fNzAfukvyh84cIAroFaLTdRT3IZR0amkWG8KSg5WWvv80lv0fO-zthT6kZDfPrSAHg0Opvtzy00ll~0lPq8V69DK3BP51GxIBiUPShjD1WgSrJsLbB7TLpug5PgTeeBRx80W0I6HIVxmRWQBOdmM~ziHQyNs8EhtCD7lYks8izHxCquCsFTuflp9IdrCIAQ__";
|
||||
|
||||
interface SwarmPanelProps {
|
||||
onSelectAgent: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function SwarmPanel({ onSelectAgent }: SwarmPanelProps) {
|
||||
const activeAgents = AGENT_CATALOG.filter(a => a.status === "active");
|
||||
const plannedAgents = AGENT_CATALOG.filter(a => a.status === "planned");
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col lg:flex-row">
|
||||
{/* Swarm visualization */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Network image */}
|
||||
<div className="relative h-[300px] lg:h-[400px] overflow-hidden border-b border-border">
|
||||
<img
|
||||
src={SWARM_IMG}
|
||||
alt="Swarm Network Topology"
|
||||
className="w-full h-full object-cover opacity-70"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/40 to-transparent" />
|
||||
<div className="absolute bottom-4 left-4 right-4">
|
||||
<div className="text-[11px] uppercase tracking-[0.15em] text-btc-orange mb-1">
|
||||
// SWARM TOPOLOGY
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-[11px] text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="status-dot status-dot-active" />
|
||||
{activeAgents.length} active
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="status-dot status-dot-planned" />
|
||||
{plannedAgents.length} planned
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Network className="w-3 h-3" />
|
||||
{AGENT_CATALOG.length} total
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent grid */}
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-4">
|
||||
<div className="text-[11px] uppercase tracking-[0.15em] text-muted-foreground mb-3">
|
||||
// REGISTERED AGENTS
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{AGENT_CATALOG.map((agent, i) => (
|
||||
<motion.button
|
||||
key={agent.id}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
onClick={() => onSelectAgent(agent.id)}
|
||||
className="panel text-left p-3 hover:bg-panel-hover transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className={`status-dot ${
|
||||
agent.status === 'active' ? 'status-dot-active' : 'status-dot-planned'
|
||||
}`} />
|
||||
<span className="text-[12px] font-semibold text-foreground">{agent.name}</span>
|
||||
<Badge variant="outline" className="text-[9px] h-4 px-1.5 border-border ml-auto">
|
||||
{agent.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground mb-1.5">{agent.role}</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{agent.capabilities.map(cap => (
|
||||
<span key={cap} className="text-[9px] px-1.5 py-0.5 bg-accent text-accent-foreground">
|
||||
{cap}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* Live event feed */}
|
||||
<div className="lg:w-[300px] border-t lg:border-t-0 lg:border-l border-border flex flex-col">
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<Radio className="w-3 h-3 text-electric-green" />
|
||||
// LIVE FEED
|
||||
<span className="ml-auto flex items-center gap-1 text-electric-green">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-electric-green animate-pulse" />
|
||||
LIVE
|
||||
</span>
|
||||
</div>
|
||||
<ScrollArea className="flex-1 max-h-[300px] lg:max-h-none">
|
||||
<div className="p-2 space-y-1">
|
||||
{[...MOCK_WS_EVENTS].reverse().map((evt, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, x: 16 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: i * 0.03 }}
|
||||
className="px-3 py-2 text-[11px] border-b border-border/30"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 mb-0.5">
|
||||
<span className={`text-[9px] font-semibold uppercase ${getEventColor(evt.event)}`}>
|
||||
{evt.event.replace(/_/g, ' ')}
|
||||
</span>
|
||||
<span className="text-[9px] text-muted-foreground ml-auto">
|
||||
{new Date(evt.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-[10px]">
|
||||
{formatEventData(evt)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getEventColor(event: string): string {
|
||||
if (event.includes('completed')) return 'text-electric-green';
|
||||
if (event.includes('assigned')) return 'text-btc-orange';
|
||||
if (event.includes('bid')) return 'text-warning-amber';
|
||||
if (event.includes('joined')) return 'text-cyber-cyan';
|
||||
if (event.includes('posted')) return 'text-foreground';
|
||||
return 'text-muted-foreground';
|
||||
}
|
||||
|
||||
function formatEventData(evt: { event: string; data: Record<string, unknown> }): string {
|
||||
const d = evt.data;
|
||||
if (evt.event === 'agent_joined') return `${d.name} joined the swarm`;
|
||||
if (evt.event === 'task_posted') return `"${d.description}"`;
|
||||
if (evt.event === 'task_assigned') return `→ ${d.agent_id}`;
|
||||
if (evt.event === 'task_completed') return `✓ ${d.agent_id}: ${d.result}`;
|
||||
if (evt.event === 'bid_submitted') return `${d.agent_id} bid ${d.bid_sats} sats`;
|
||||
return JSON.stringify(d);
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Task management panel
|
||||
* Task list with status badges, filtering, and auction indicators
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MOCK_TASKS, type Task } from "@/lib/data";
|
||||
import { motion } from "framer-motion";
|
||||
import { ListTodo, Plus, Filter, Clock, CheckCircle, AlertCircle, Loader2, Gavel } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface TasksPanelProps {
|
||||
onSelectTask: (id: string) => void;
|
||||
}
|
||||
|
||||
type StatusFilter = "all" | Task["status"];
|
||||
|
||||
export default function TasksPanel({ onSelectTask }: TasksPanelProps) {
|
||||
const [filter, setFilter] = useState<StatusFilter>("all");
|
||||
const [tasks] = useState<Task[]>(MOCK_TASKS);
|
||||
|
||||
const filtered = filter === "all" ? tasks : tasks.filter(t => t.status === filter);
|
||||
|
||||
const statusCounts = {
|
||||
all: tasks.length,
|
||||
pending: tasks.filter(t => t.status === "pending").length,
|
||||
bidding: tasks.filter(t => t.status === "bidding").length,
|
||||
assigned: tasks.filter(t => t.status === "assigned").length,
|
||||
running: tasks.filter(t => t.status === "running").length,
|
||||
completed: tasks.filter(t => t.status === "completed").length,
|
||||
failed: tasks.filter(t => t.status === "failed").length,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header with filters */}
|
||||
<div className="border-b border-border p-3">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-[11px] uppercase tracking-[0.15em] text-muted-foreground flex items-center gap-2">
|
||||
<ListTodo className="w-3 h-3" />
|
||||
// TASK QUEUE
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-[11px] text-btc-orange hover:text-btc-orange/80 hover:bg-btc-orange/10"
|
||||
onClick={() => toast("Feature coming soon", { description: "Task creation requires backend connection." })}
|
||||
>
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
NEW TASK
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Filter pills */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{(["all", "pending", "bidding", "running", "completed"] as StatusFilter[]).map(s => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setFilter(s)}
|
||||
className={`text-[10px] uppercase tracking-wider px-2 py-1 transition-colors ${
|
||||
filter === s
|
||||
? 'bg-btc-orange/20 text-btc-orange border border-btc-orange/30'
|
||||
: 'bg-accent text-muted-foreground border border-transparent hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{s} ({statusCounts[s]})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task list */}
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-3 space-y-2">
|
||||
{filtered.map((task, i) => (
|
||||
<motion.button
|
||||
key={task.id}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
onClick={() => onSelectTask(task.id)}
|
||||
className="panel w-full text-left p-3 hover:bg-panel-hover transition-colors"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<StatusIcon status={task.status} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-[10px] text-muted-foreground font-mono">{task.id}</span>
|
||||
<StatusBadge status={task.status} />
|
||||
</div>
|
||||
<p className="text-[12px] text-foreground leading-snug mb-1.5">
|
||||
{task.description}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 text-[10px] text-muted-foreground">
|
||||
{task.assignedAgent && (
|
||||
<span className="flex items-center gap-1">
|
||||
→ <span className="text-btc-orange">{task.assignedAgent}</span>
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-2.5 h-2.5" />
|
||||
{new Date(task.createdAt).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.button>
|
||||
))}
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<div className="text-center py-12 text-muted-foreground text-[12px]">
|
||||
No tasks matching filter "{filter}"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusIcon({ status }: { status: Task["status"] }) {
|
||||
const cls = "w-4 h-4 mt-0.5 flex-shrink-0";
|
||||
switch (status) {
|
||||
case "pending": return <Clock className={`${cls} text-muted-foreground`} />;
|
||||
case "bidding": return <Gavel className={`${cls} text-warning-amber`} />;
|
||||
case "assigned": return <AlertCircle className={`${cls} text-cyber-cyan`} />;
|
||||
case "running": return <Loader2 className={`${cls} text-btc-orange animate-spin`} />;
|
||||
case "completed": return <CheckCircle className={`${cls} text-electric-green`} />;
|
||||
case "failed": return <AlertCircle className={`${cls} text-danger-red`} />;
|
||||
}
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: Task["status"] }) {
|
||||
const colors: Record<Task["status"], string> = {
|
||||
pending: "bg-muted text-muted-foreground",
|
||||
bidding: "bg-warning-amber/20 text-warning-amber border-warning-amber/30",
|
||||
assigned: "bg-cyber-cyan/20 text-cyber-cyan border-cyber-cyan/30",
|
||||
running: "bg-btc-orange/20 text-btc-orange border-btc-orange/30",
|
||||
completed: "bg-electric-green/20 text-electric-green border-electric-green/30",
|
||||
failed: "bg-danger-red/20 text-danger-red border-danger-red/30",
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`text-[9px] uppercase tracking-wider px-1.5 py-0.5 border ${colors[status]}`}>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Top navigation bar
|
||||
* Bitcoin orange accent line at top, system title, notification bell
|
||||
*/
|
||||
|
||||
import { Bell, Menu, Terminal, Zap } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MOCK_HEALTH } from "@/lib/data";
|
||||
|
||||
interface TopBarProps {
|
||||
unreadCount: number;
|
||||
onToggleSidebar: () => void;
|
||||
}
|
||||
|
||||
export default function TopBar({ unreadCount, onToggleSidebar }: TopBarProps) {
|
||||
return (
|
||||
<header className="h-12 flex-shrink-0 border-b border-border bg-card flex items-center px-4 gap-3"
|
||||
style={{ borderTop: '2px solid oklch(0.75 0.18 55)' }}
|
||||
>
|
||||
{/* Mobile menu button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="lg:hidden h-8 w-8 text-muted-foreground hover:text-foreground"
|
||||
onClick={onToggleSidebar}
|
||||
>
|
||||
<Menu className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{/* Logo / Title */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="w-4 h-4 text-btc-orange" />
|
||||
<span className="text-[13px] font-semibold tracking-[0.05em] text-foreground">
|
||||
TIMMY TIME
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground tracking-[0.1em]">
|
||||
MISSION CONTROL
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Spacer */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Status indicators */}
|
||||
<div className="hidden sm:flex items-center gap-4 text-[11px] text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={`status-dot ${MOCK_HEALTH.ollama === 'up' ? 'status-dot-active' : 'status-dot-danger'}`} />
|
||||
<span>OLLAMA</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={`status-dot ${MOCK_HEALTH.swarmRegistry === 'active' ? 'status-dot-active' : 'status-dot-warning'}`} />
|
||||
<span>SWARM</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Zap className="w-3 h-3 text-btc-orange" />
|
||||
<span className="text-btc-orange font-medium">
|
||||
{MOCK_HEALTH.l402Balance.toLocaleString()} sats
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notifications */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="relative h-8 w-8 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Bell className="w-4 h-4" />
|
||||
{unreadCount > 0 && (
|
||||
<span className="absolute -top-0.5 -right-0.5 w-4 h-4 rounded-full bg-btc-orange text-[9px] font-bold text-black flex items-center justify-center">
|
||||
{unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Version */}
|
||||
<span className="text-[9px] text-muted-foreground tracking-wider hidden md:inline">
|
||||
v2.0.0
|
||||
</span>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Hacker Aesthetic with Bitcoin Soul
|
||||
* Base: True black (#000000) with blue tint on panels (#080c12)
|
||||
* Primary: Bitcoin orange (#f7931a)
|
||||
* Active/Health: Electric green (#39ff14)
|
||||
* Text: White (#e8e8e8) > Steel gray (#6b7280) > Dark gray (#374151)
|
||||
* Font: JetBrains Mono throughout
|
||||
*/
|
||||
|
||||
@theme inline {
|
||||
--font-sans: 'JetBrains Mono', monospace;
|
||||
--font-mono: 'Fira Code', 'JetBrains Mono', monospace;
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
/* Custom sovereign colors */
|
||||
--color-btc-orange: oklch(0.75 0.18 55);
|
||||
--color-btc-orange-dim: oklch(0.55 0.14 55);
|
||||
--color-electric-green: oklch(0.85 0.3 145);
|
||||
--color-electric-green-dim: oklch(0.55 0.2 145);
|
||||
--color-cyber-cyan: oklch(0.8 0.15 200);
|
||||
--color-warning-amber: oklch(0.8 0.18 80);
|
||||
--color-danger-red: oklch(0.65 0.25 25);
|
||||
--color-panel: oklch(0.12 0.01 260);
|
||||
--color-panel-hover: oklch(0.16 0.01 260);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.25rem;
|
||||
--background: oklch(0.05 0.005 260);
|
||||
--foreground: oklch(0.9 0.005 90);
|
||||
--card: oklch(0.1 0.008 260);
|
||||
--card-foreground: oklch(0.9 0.005 90);
|
||||
--popover: oklch(0.12 0.01 260);
|
||||
--popover-foreground: oklch(0.9 0.005 90);
|
||||
--primary: oklch(0.75 0.18 55);
|
||||
--primary-foreground: oklch(0.1 0.005 55);
|
||||
--secondary: oklch(0.15 0.008 260);
|
||||
--secondary-foreground: oklch(0.7 0.01 90);
|
||||
--muted: oklch(0.18 0.008 260);
|
||||
--muted-foreground: oklch(0.55 0.01 260);
|
||||
--accent: oklch(0.15 0.01 260);
|
||||
--accent-foreground: oklch(0.9 0.005 90);
|
||||
--destructive: oklch(0.55 0.22 25);
|
||||
--destructive-foreground: oklch(0.95 0 0);
|
||||
--border: oklch(0.22 0.01 260);
|
||||
--input: oklch(0.18 0.01 260);
|
||||
--ring: oklch(0.75 0.18 55);
|
||||
--chart-1: oklch(0.75 0.18 55);
|
||||
--chart-2: oklch(0.85 0.3 145);
|
||||
--chart-3: oklch(0.8 0.15 200);
|
||||
--chart-4: oklch(0.8 0.18 80);
|
||||
--chart-5: oklch(0.65 0.25 25);
|
||||
--sidebar: oklch(0.08 0.008 260);
|
||||
--sidebar-foreground: oklch(0.85 0.005 90);
|
||||
--sidebar-primary: oklch(0.75 0.18 55);
|
||||
--sidebar-primary-foreground: oklch(0.1 0.005 55);
|
||||
--sidebar-accent: oklch(0.14 0.01 260);
|
||||
--sidebar-accent-foreground: oklch(0.9 0.005 90);
|
||||
--sidebar-border: oklch(0.2 0.01 260);
|
||||
--sidebar-ring: oklch(0.75 0.18 55);
|
||||
}
|
||||
|
||||
/* Dark is the only theme — no light mode */
|
||||
.dark {
|
||||
--background: oklch(0.05 0.005 260);
|
||||
--foreground: oklch(0.9 0.005 90);
|
||||
--card: oklch(0.1 0.008 260);
|
||||
--card-foreground: oklch(0.9 0.005 90);
|
||||
--popover: oklch(0.12 0.01 260);
|
||||
--popover-foreground: oklch(0.9 0.005 90);
|
||||
--primary: oklch(0.75 0.18 55);
|
||||
--primary-foreground: oklch(0.1 0.005 55);
|
||||
--secondary: oklch(0.15 0.008 260);
|
||||
--secondary-foreground: oklch(0.7 0.01 90);
|
||||
--muted: oklch(0.18 0.008 260);
|
||||
--muted-foreground: oklch(0.55 0.01 260);
|
||||
--accent: oklch(0.15 0.01 260);
|
||||
--accent-foreground: oklch(0.9 0.005 90);
|
||||
--destructive: oklch(0.55 0.22 25);
|
||||
--destructive-foreground: oklch(0.95 0 0);
|
||||
--border: oklch(0.22 0.01 260);
|
||||
--input: oklch(0.18 0.01 260);
|
||||
--ring: oklch(0.75 0.18 55);
|
||||
--chart-1: oklch(0.75 0.18 55);
|
||||
--chart-2: oklch(0.85 0.3 145);
|
||||
--chart-3: oklch(0.8 0.15 200);
|
||||
--chart-4: oklch(0.8 0.18 80);
|
||||
--chart-5: oklch(0.65 0.25 25);
|
||||
--sidebar: oklch(0.08 0.008 260);
|
||||
--sidebar-foreground: oklch(0.85 0.005 90);
|
||||
--sidebar-primary: oklch(0.75 0.18 55);
|
||||
--sidebar-primary-foreground: oklch(0.1 0.005 55);
|
||||
--sidebar-accent: oklch(0.14 0.01 260);
|
||||
--sidebar-accent-foreground: oklch(0.9 0.005 90);
|
||||
--sidebar-border: oklch(0.2 0.01 260);
|
||||
--sidebar-ring: oklch(0.75 0.18 55);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[role="button"]:not([aria-disabled="true"]),
|
||||
[type="button"]:not(:disabled),
|
||||
[type="submit"]:not(:disabled),
|
||||
[type="reset"]:not(:disabled),
|
||||
a[href],
|
||||
select:not(:disabled),
|
||||
input[type="checkbox"]:not(:disabled),
|
||||
input[type="radio"]:not(:disabled) {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.container {
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.flex {
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
max-width: 1600px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sovereign Terminal custom components */
|
||||
.panel {
|
||||
@apply bg-card border border-border;
|
||||
border-top: 2px solid var(--color-btc-orange);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
@apply text-[11px] uppercase tracking-[0.15em] text-muted-foreground px-4 py-2 border-b border-border;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
@apply inline-block w-2 h-2 rounded-full;
|
||||
}
|
||||
|
||||
.status-dot-active {
|
||||
@apply bg-electric-green;
|
||||
box-shadow: 0 0 6px var(--color-electric-green);
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-dot-warning {
|
||||
@apply bg-warning-amber;
|
||||
box-shadow: 0 0 6px var(--color-warning-amber);
|
||||
}
|
||||
|
||||
.status-dot-danger {
|
||||
@apply bg-danger-red;
|
||||
box-shadow: 0 0 6px var(--color-danger-red);
|
||||
}
|
||||
|
||||
.status-dot-planned {
|
||||
@apply bg-muted-foreground;
|
||||
}
|
||||
|
||||
.terminal-prompt {
|
||||
@apply text-btc-orange font-semibold;
|
||||
}
|
||||
|
||||
.sat-amount {
|
||||
@apply text-btc-orange font-mono font-medium;
|
||||
}
|
||||
|
||||
/* Scanline overlay */
|
||||
.scanline-overlay {
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba(0, 0, 0, 0.03) 2px,
|
||||
rgba(0, 0, 0, 0.03) 4px
|
||||
);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
@keyframes typewriter-cursor {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes scan-line {
|
||||
0% { transform: translateY(-100%); }
|
||||
100% { transform: translateY(100vh); }
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-right {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(16px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: oklch(0.3 0.01 260);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: oklch(0.4 0.01 260);
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Hacker Aesthetic with Bitcoin Soul
|
||||
* Static data for the dashboard — mirrors the Python backend models
|
||||
*/
|
||||
|
||||
export interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
description: string;
|
||||
capabilities: string[];
|
||||
rateSats: number;
|
||||
status: "active" | "planned" | "offline";
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
description: string;
|
||||
status: "pending" | "bidding" | "assigned" | "running" | "completed" | "failed";
|
||||
assignedAgent: string | null;
|
||||
result: string | null;
|
||||
createdAt: string;
|
||||
completedAt: string | null;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
id: number;
|
||||
title: string;
|
||||
message: string;
|
||||
category: "swarm" | "task" | "agent" | "system" | "payment";
|
||||
timestamp: string;
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
role: "user" | "assistant" | "system";
|
||||
content: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface WSEvent {
|
||||
event: string;
|
||||
data: Record<string, unknown>;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// ── Agent Catalog ─────────────────────────────────────────────────────────
|
||||
export const AGENT_CATALOG: Agent[] = [
|
||||
{
|
||||
id: "timmy",
|
||||
name: "Timmy",
|
||||
role: "Sovereign Commander",
|
||||
description: "Primary AI companion. Coordinates the swarm, manages tasks, and maintains sovereignty.",
|
||||
capabilities: ["chat", "reasoning", "coordination"],
|
||||
rateSats: 0,
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: "echo",
|
||||
name: "Echo",
|
||||
role: "Research Analyst",
|
||||
description: "Deep research and information synthesis. Reads, summarizes, and cross-references sources.",
|
||||
capabilities: ["research", "summarization", "fact-checking"],
|
||||
rateSats: 50,
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
id: "mace",
|
||||
name: "Mace",
|
||||
role: "Security Sentinel",
|
||||
description: "Network security, threat assessment, and system hardening recommendations.",
|
||||
capabilities: ["security", "monitoring", "threat-analysis"],
|
||||
rateSats: 75,
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
id: "helm",
|
||||
name: "Helm",
|
||||
role: "System Navigator",
|
||||
description: "Infrastructure management, deployment automation, and system configuration.",
|
||||
capabilities: ["devops", "automation", "configuration"],
|
||||
rateSats: 60,
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
id: "seer",
|
||||
name: "Seer",
|
||||
role: "Data Oracle",
|
||||
description: "Data analysis, pattern recognition, and predictive insights from local datasets.",
|
||||
capabilities: ["analytics", "visualization", "prediction"],
|
||||
rateSats: 65,
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
id: "forge",
|
||||
name: "Forge",
|
||||
role: "Code Smith",
|
||||
description: "Code generation, refactoring, debugging, and test writing.",
|
||||
capabilities: ["coding", "debugging", "testing"],
|
||||
rateSats: 55,
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
id: "quill",
|
||||
name: "Quill",
|
||||
role: "Content Scribe",
|
||||
description: "Long-form writing, editing, documentation, and content creation.",
|
||||
capabilities: ["writing", "editing", "documentation"],
|
||||
rateSats: 45,
|
||||
status: "planned",
|
||||
},
|
||||
];
|
||||
|
||||
// ── Mock Tasks ────────────────────────────────────────────────────────────
|
||||
export const MOCK_TASKS: Task[] = [
|
||||
{
|
||||
id: "t-001",
|
||||
description: "Analyze Bitcoin whitepaper and summarize key innovations",
|
||||
status: "completed",
|
||||
assignedAgent: "timmy",
|
||||
result: "Summary generated: 3 key innovations identified — decentralized consensus, proof-of-work, and UTXO model.",
|
||||
createdAt: "2026-02-21T10:00:00Z",
|
||||
completedAt: "2026-02-21T10:02:30Z",
|
||||
},
|
||||
{
|
||||
id: "t-002",
|
||||
description: "Scan local network for open ports and vulnerabilities",
|
||||
status: "bidding",
|
||||
assignedAgent: null,
|
||||
result: null,
|
||||
createdAt: "2026-02-21T14:30:00Z",
|
||||
completedAt: null,
|
||||
},
|
||||
{
|
||||
id: "t-003",
|
||||
description: "Generate unit tests for the L402 proxy module",
|
||||
status: "running",
|
||||
assignedAgent: "forge",
|
||||
result: null,
|
||||
createdAt: "2026-02-21T15:00:00Z",
|
||||
completedAt: null,
|
||||
},
|
||||
{
|
||||
id: "t-004",
|
||||
description: "Write documentation for the swarm coordinator API",
|
||||
status: "pending",
|
||||
assignedAgent: null,
|
||||
result: null,
|
||||
createdAt: "2026-02-21T16:00:00Z",
|
||||
completedAt: null,
|
||||
},
|
||||
{
|
||||
id: "t-005",
|
||||
description: "Research self-custody best practices for 2026",
|
||||
status: "assigned",
|
||||
assignedAgent: "echo",
|
||||
result: null,
|
||||
createdAt: "2026-02-21T16:30:00Z",
|
||||
completedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Mock Notifications ────────────────────────────────────────────────────
|
||||
export const MOCK_NOTIFICATIONS: Notification[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Swarm Online",
|
||||
message: "Timmy coordinator initialized. Swarm registry active.",
|
||||
category: "system",
|
||||
timestamp: "2026-02-21T10:00:00Z",
|
||||
read: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Task Completed",
|
||||
message: "Bitcoin whitepaper analysis finished in 2m 30s.",
|
||||
category: "task",
|
||||
timestamp: "2026-02-21T10:02:30Z",
|
||||
read: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Auction Started",
|
||||
message: "Network scan task open for bidding. 15s auction window.",
|
||||
category: "swarm",
|
||||
timestamp: "2026-02-21T14:30:00Z",
|
||||
read: false,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Agent Assigned",
|
||||
message: "Forge won the bid for test generation at 55 sats.",
|
||||
category: "agent",
|
||||
timestamp: "2026-02-21T15:00:05Z",
|
||||
read: false,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "L402 Payment",
|
||||
message: "Invoice settled: 75 sats for Mace security scan.",
|
||||
category: "payment",
|
||||
timestamp: "2026-02-21T15:30:00Z",
|
||||
read: false,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Mock WebSocket Events ─────────────────────────────────────────────────
|
||||
export const MOCK_WS_EVENTS: WSEvent[] = [
|
||||
{ event: "agent_joined", data: { agent_id: "timmy", name: "Timmy" }, timestamp: "2026-02-21T10:00:00Z" },
|
||||
{ event: "task_posted", data: { task_id: "t-001", description: "Analyze Bitcoin whitepaper" }, timestamp: "2026-02-21T10:00:05Z" },
|
||||
{ event: "task_assigned", data: { task_id: "t-001", agent_id: "timmy" }, timestamp: "2026-02-21T10:00:10Z" },
|
||||
{ event: "task_completed", data: { task_id: "t-001", agent_id: "timmy", result: "Analysis complete" }, timestamp: "2026-02-21T10:02:30Z" },
|
||||
{ event: "task_posted", data: { task_id: "t-002", description: "Scan local network" }, timestamp: "2026-02-21T14:30:00Z" },
|
||||
{ event: "bid_submitted", data: { task_id: "t-002", agent_id: "mace", bid_sats: 75 }, timestamp: "2026-02-21T14:30:05Z" },
|
||||
{ event: "task_posted", data: { task_id: "t-003", description: "Generate unit tests" }, timestamp: "2026-02-21T15:00:00Z" },
|
||||
{ event: "task_assigned", data: { task_id: "t-003", agent_id: "forge" }, timestamp: "2026-02-21T15:00:05Z" },
|
||||
];
|
||||
|
||||
// ── Mock Chat History ─────────────────────────────────────────────────────
|
||||
export const MOCK_CHAT: ChatMessage[] = [
|
||||
{
|
||||
id: "c-001",
|
||||
role: "system",
|
||||
content: "// TIMMY v1.0.0 — Sovereign Commander online\n// Ollama backend: llama3.2\n// All systems nominal. No cloud. No telemetry.",
|
||||
timestamp: "2026-02-21T10:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "c-002",
|
||||
role: "user",
|
||||
content: "What is sovereignty in the context of Bitcoin?",
|
||||
timestamp: "2026-02-21T10:01:00Z",
|
||||
},
|
||||
{
|
||||
id: "c-003",
|
||||
role: "assistant",
|
||||
content: "Sovereignty in Bitcoin means self-custody — you hold your own keys, you validate your own transactions, you run your own node. No intermediary can freeze your funds, censor your payments, or inflate away your savings.\n\nThe same principle applies here: Timmy runs locally on your hardware, uses Ollama for inference, and stores everything in SQLite. No cloud dependency. No API keys to revoke. Your AI, your rules.\n\nSats are sovereignty, boss.",
|
||||
timestamp: "2026-02-21T10:01:15Z",
|
||||
},
|
||||
];
|
||||
|
||||
// ── System Health ─────────────────────────────────────────────────────────
|
||||
export interface SystemHealth {
|
||||
ollama: "up" | "down" | "unknown";
|
||||
model: string;
|
||||
swarmRegistry: "active" | "inactive";
|
||||
activeAgents: number;
|
||||
totalTasks: number;
|
||||
completedTasks: number;
|
||||
uptime: string;
|
||||
l402Balance: number;
|
||||
}
|
||||
|
||||
export const MOCK_HEALTH: SystemHealth = {
|
||||
ollama: "up",
|
||||
model: "llama3.2",
|
||||
swarmRegistry: "active",
|
||||
activeAgents: 1,
|
||||
totalTasks: 5,
|
||||
completedTasks: 1,
|
||||
uptime: "4h 21m",
|
||||
l402Balance: 12_450,
|
||||
};
|
||||
|
||||
// ── Voice NLU Intents ─────────────────────────────────────────────────────
|
||||
export const VOICE_INTENTS = [
|
||||
{ name: "chat", description: "General conversation", example: "Tell me about self-custody" },
|
||||
{ name: "status", description: "System status query", example: "How are you?" },
|
||||
{ name: "swarm", description: "Swarm management", example: "Spawn agent Echo" },
|
||||
{ name: "task", description: "Task management", example: "Create task: scan network" },
|
||||
{ name: "help", description: "List commands", example: "What can you do?" },
|
||||
{ name: "voice", description: "Voice settings", example: "Speak slower" },
|
||||
];
|
||||
|
||||
// ── Roadmap ───────────────────────────────────────────────────────────────
|
||||
export const ROADMAP = [
|
||||
{ version: "1.0.0", name: "Genesis", milestone: "Agno + Ollama + SQLite + Dashboard", status: "complete" as const },
|
||||
{ version: "2.0.0", name: "Exodus", milestone: "MCP tools + multi-agent swarm", status: "current" as const },
|
||||
{ version: "3.0.0", name: "Revelation", milestone: "Bitcoin Lightning treasury + single .app", status: "planned" as const },
|
||||
];
|
||||
@@ -1,5 +0,0 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./index.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(<App />);
|
||||
@@ -1,181 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Hacker Aesthetic with Bitcoin Soul
|
||||
* Three-column asymmetric layout:
|
||||
* Left (narrow): Status cards — agents, health, notifications, L402
|
||||
* Center (wide): Active workspace — Chat, Swarm, Tasks, Marketplace tabs
|
||||
* Right (medium): Context panel — details, auctions, invoices
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
Terminal, Cpu, Zap, Bell, Shield,
|
||||
MessageSquare, Network, ListTodo, Store,
|
||||
ChevronRight, Activity, Volume2
|
||||
} from "lucide-react";
|
||||
|
||||
import StatusSidebar from "@/components/StatusSidebar";
|
||||
import ChatPanel from "@/components/ChatPanel";
|
||||
import SwarmPanel from "@/components/SwarmPanel";
|
||||
import TasksPanel from "@/components/TasksPanel";
|
||||
import MarketplacePanel from "@/components/MarketplacePanel";
|
||||
import ContextPanel from "@/components/ContextPanel";
|
||||
import TopBar from "@/components/TopBar";
|
||||
import { MOCK_NOTIFICATIONS } from "@/lib/data";
|
||||
|
||||
type TabValue = "chat" | "swarm" | "tasks" | "marketplace";
|
||||
|
||||
export default function Dashboard() {
|
||||
const [activeTab, setActiveTab] = useState<TabValue>("chat");
|
||||
const [selectedAgent, setSelectedAgent] = useState<string | null>("timmy");
|
||||
const [selectedTask, setSelectedTask] = useState<string | null>(null);
|
||||
const [showMobileSidebar, setShowMobileSidebar] = useState(false);
|
||||
const unreadCount = MOCK_NOTIFICATIONS.filter(n => !n.read).length;
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col overflow-hidden bg-background">
|
||||
{/* Scanline overlay */}
|
||||
<div className="scanline-overlay" />
|
||||
|
||||
{/* Top bar */}
|
||||
<TopBar
|
||||
unreadCount={unreadCount}
|
||||
onToggleSidebar={() => setShowMobileSidebar(!showMobileSidebar)}
|
||||
/>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Left sidebar — status panels */}
|
||||
<aside className="hidden lg:flex w-[280px] flex-shrink-0 flex-col border-r border-border overflow-hidden">
|
||||
<ScrollArea className="flex-1">
|
||||
<StatusSidebar
|
||||
onSelectAgent={(id) => {
|
||||
setSelectedAgent(id);
|
||||
setSelectedTask(null);
|
||||
}}
|
||||
selectedAgent={selectedAgent}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</aside>
|
||||
|
||||
{/* Mobile sidebar overlay */}
|
||||
<AnimatePresence>
|
||||
{showMobileSidebar && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0.5 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="lg:hidden fixed inset-0 bg-black z-40"
|
||||
onClick={() => setShowMobileSidebar(false)}
|
||||
/>
|
||||
<motion.aside
|
||||
initial={{ x: -280 }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -280 }}
|
||||
transition={{ type: "tween", duration: 0.2 }}
|
||||
className="lg:hidden fixed left-0 top-0 bottom-0 w-[280px] bg-background border-r border-border z-50 overflow-auto"
|
||||
>
|
||||
<div className="pt-14">
|
||||
<StatusSidebar
|
||||
onSelectAgent={(id) => {
|
||||
setSelectedAgent(id);
|
||||
setSelectedTask(null);
|
||||
setShowMobileSidebar(false);
|
||||
}}
|
||||
selectedAgent={selectedAgent}
|
||||
/>
|
||||
</div>
|
||||
</motion.aside>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Center workspace */}
|
||||
<main className="flex-1 flex flex-col overflow-hidden">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => setActiveTab(v as TabValue)}
|
||||
className="flex-1 flex flex-col overflow-hidden"
|
||||
>
|
||||
<div className="border-b border-border px-4">
|
||||
<TabsList className="bg-transparent h-10 gap-0 p-0">
|
||||
<TabsTrigger
|
||||
value="chat"
|
||||
className="data-[state=active]:bg-transparent data-[state=active]:text-btc-orange data-[state=active]:border-b-2 data-[state=active]:border-btc-orange rounded-none px-4 text-[11px] uppercase tracking-[0.12em] text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<MessageSquare className="w-3.5 h-3.5 mr-1.5" />
|
||||
Chat
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="swarm"
|
||||
className="data-[state=active]:bg-transparent data-[state=active]:text-btc-orange data-[state=active]:border-b-2 data-[state=active]:border-btc-orange rounded-none px-4 text-[11px] uppercase tracking-[0.12em] text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Network className="w-3.5 h-3.5 mr-1.5" />
|
||||
Swarm
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="tasks"
|
||||
className="data-[state=active]:bg-transparent data-[state=active]:text-btc-orange data-[state=active]:border-b-2 data-[state=active]:border-btc-orange rounded-none px-4 text-[11px] uppercase tracking-[0.12em] text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ListTodo className="w-3.5 h-3.5 mr-1.5" />
|
||||
Tasks
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="marketplace"
|
||||
className="data-[state=active]:bg-transparent data-[state=active]:text-btc-orange data-[state=active]:border-b-2 data-[state=active]:border-btc-orange rounded-none px-4 text-[11px] uppercase tracking-[0.12em] text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Store className="w-3.5 h-3.5 mr-1.5" />
|
||||
Marketplace
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<TabsContent value="chat" className="h-full m-0 p-0">
|
||||
<ChatPanel />
|
||||
</TabsContent>
|
||||
<TabsContent value="swarm" className="h-full m-0 p-0">
|
||||
<SwarmPanel
|
||||
onSelectAgent={(id) => {
|
||||
setSelectedAgent(id);
|
||||
setSelectedTask(null);
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="tasks" className="h-full m-0 p-0">
|
||||
<TasksPanel
|
||||
onSelectTask={(id) => {
|
||||
setSelectedTask(id);
|
||||
setSelectedAgent(null);
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="marketplace" className="h-full m-0 p-0">
|
||||
<MarketplacePanel
|
||||
onSelectAgent={(id) => {
|
||||
setSelectedAgent(id);
|
||||
setSelectedTask(null);
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</main>
|
||||
|
||||
{/* Right context panel */}
|
||||
<aside className="hidden xl:flex w-[320px] flex-shrink-0 flex-col border-l border-border overflow-hidden">
|
||||
<ScrollArea className="flex-1">
|
||||
<ContextPanel
|
||||
selectedAgent={selectedAgent}
|
||||
selectedTask={selectedTask}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Redirect } from "wouter";
|
||||
|
||||
export default function Home() {
|
||||
return <Redirect to="/" />;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Terminal } from "lucide-react";
|
||||
import { useLocation } from "wouter";
|
||||
|
||||
export default function NotFound() {
|
||||
const [, setLocation] = useLocation();
|
||||
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-center space-y-4">
|
||||
<Terminal className="w-12 h-12 text-btc-orange mx-auto" />
|
||||
<div className="text-[11px] uppercase tracking-[0.15em] text-btc-orange">
|
||||
// ERROR 404
|
||||
</div>
|
||||
<h1 className="text-[24px] font-bold text-foreground">
|
||||
Route not found
|
||||
</h1>
|
||||
<p className="text-[13px] text-muted-foreground max-w-sm mx-auto">
|
||||
The requested path does not exist in Mission Control.
|
||||
Check the URL or return to the dashboard.
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-[11px] text-btc-orange hover:bg-btc-orange/10 border border-btc-orange/30 mt-4"
|
||||
onClick={() => setLocation("/")}
|
||||
>
|
||||
RETURN TO MISSION CONTROL
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -159,13 +159,17 @@ async function sendMobileMessage(event) {
|
||||
|
||||
const chat = document.getElementById('mobile-chat');
|
||||
|
||||
// Add user message
|
||||
chat.innerHTML += `
|
||||
<div class="chat-message user">
|
||||
<div class="chat-meta">You</div>
|
||||
<div>${message}</div>
|
||||
</div>
|
||||
`;
|
||||
// Add user message — use DOM methods to avoid XSS
|
||||
const userDiv = document.createElement('div');
|
||||
userDiv.className = 'chat-message user';
|
||||
const userMeta = document.createElement('div');
|
||||
userMeta.className = 'chat-meta';
|
||||
userMeta.textContent = 'You';
|
||||
const userText = document.createElement('div');
|
||||
userText.textContent = message; // textContent escapes HTML
|
||||
userDiv.appendChild(userMeta);
|
||||
userDiv.appendChild(userText);
|
||||
chat.appendChild(userDiv);
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
|
||||
input.value = '';
|
||||
|
||||
@@ -104,50 +104,69 @@ function handleMessage(message) {
|
||||
}
|
||||
}
|
||||
|
||||
// Safe text setter — avoids XSS when inserting user/server data into DOM
|
||||
function _t(el, text) { el.textContent = text; return el; }
|
||||
function _el(tag, cls) { const e = document.createElement(tag); if (cls) e.className = cls; return e; }
|
||||
|
||||
function updateAgentsList(agents) {
|
||||
const container = document.getElementById('agents-list');
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!agents || agents.length === 0) {
|
||||
container.innerHTML = '<p style="color: var(--text-muted);">No agents registered</p>';
|
||||
const p = _el('p'); p.style.color = 'var(--text-muted)';
|
||||
_t(p, 'No agents registered');
|
||||
container.appendChild(p);
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = agents.map(agent => `
|
||||
<div class="agent-card">
|
||||
<div class="agent-avatar">${agent.name.charAt(0).toUpperCase()}</div>
|
||||
<div class="agent-info">
|
||||
<div class="agent-name">${agent.name}</div>
|
||||
<div class="agent-meta">${agent.description || 'No description'}</div>
|
||||
<div class="agent-meta">
|
||||
<span class="badge badge-${agent.status === 'active' ? 'success' : agent.status === 'busy' ? 'warning' : 'danger'}">${agent.status}</span>
|
||||
${agent.min_bid} sats min bid
|
||||
| ${agent.tasks_completed} tasks
|
||||
| ${agent.total_earned} sats earned
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
agents.forEach(agent => {
|
||||
const card = _el('div', 'agent-card');
|
||||
const avatar = _el('div', 'agent-avatar');
|
||||
_t(avatar, (agent.name || '?').charAt(0).toUpperCase());
|
||||
const info = _el('div', 'agent-info');
|
||||
const name = _el('div', 'agent-name');
|
||||
_t(name, agent.name || '');
|
||||
const desc = _el('div', 'agent-meta');
|
||||
_t(desc, agent.description || 'No description');
|
||||
const meta = _el('div', 'agent-meta');
|
||||
const badge = _el('span', `badge badge-${agent.status === 'active' ? 'success' : agent.status === 'busy' ? 'warning' : 'danger'}`);
|
||||
_t(badge, agent.status || '');
|
||||
const stats = _el('span');
|
||||
_t(stats, ` ${agent.min_bid ?? 0} sats min bid | ${agent.tasks_completed ?? 0} tasks | ${agent.total_earned ?? 0} sats earned`);
|
||||
meta.appendChild(badge);
|
||||
meta.appendChild(stats);
|
||||
info.appendChild(name);
|
||||
info.appendChild(desc);
|
||||
info.appendChild(meta);
|
||||
card.appendChild(avatar);
|
||||
card.appendChild(info);
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function updateAuctionsList(auctions) {
|
||||
const container = document.getElementById('auctions-list');
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!auctions || auctions.length === 0) {
|
||||
container.innerHTML = '<p style="color: var(--text-muted);">No active auctions</p>';
|
||||
const p = _el('p'); p.style.color = 'var(--text-muted)';
|
||||
_t(p, 'No active auctions');
|
||||
container.appendChild(p);
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = auctions.map(auction => `
|
||||
<div class="agent-card">
|
||||
<div class="agent-info">
|
||||
<div class="agent-name">Task ${auction.task_id.slice(0, 8)}</div>
|
||||
<div class="agent-meta">
|
||||
${Math.round(auction.time_remaining)}s remaining
|
||||
| ${auction.bid_count} bids
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
auctions.forEach(auction => {
|
||||
const card = _el('div', 'agent-card');
|
||||
const info = _el('div', 'agent-info');
|
||||
const name = _el('div', 'agent-name');
|
||||
_t(name, 'Task ' + String(auction.task_id || '').slice(0, 8));
|
||||
const meta = _el('div', 'agent-meta');
|
||||
_t(meta, `${Math.round(auction.time_remaining ?? 0)}s remaining | ${auction.bid_count ?? 0} bids`);
|
||||
info.appendChild(name);
|
||||
info.appendChild(meta);
|
||||
card.appendChild(info);
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function addLog(message, type = 'info') {
|
||||
|
||||
@@ -279,3 +279,55 @@ def test_M605_health_status_passes_model_to_template(client):
|
||||
# The default model is llama3.2 — it should appear in the partial from settings, not hardcoded
|
||||
assert response.status_code == 200
|
||||
assert "llama3.2" in response.text # rendered via template variable, not hardcoded literal
|
||||
|
||||
|
||||
# ── M7xx — XSS prevention ─────────────────────────────────────────────────────
|
||||
|
||||
def _mobile_html() -> str:
|
||||
"""Read the mobile template source."""
|
||||
path = Path(__file__).parent.parent / "src" / "dashboard" / "templates" / "mobile.html"
|
||||
return path.read_text()
|
||||
|
||||
|
||||
def _swarm_live_html() -> str:
|
||||
"""Read the swarm live template source."""
|
||||
path = Path(__file__).parent.parent / "src" / "dashboard" / "templates" / "swarm_live.html"
|
||||
return path.read_text()
|
||||
|
||||
|
||||
def test_M701_mobile_chat_no_raw_message_interpolation():
|
||||
"""mobile.html must not interpolate ${message} directly into innerHTML — XSS risk."""
|
||||
html = _mobile_html()
|
||||
# The vulnerable pattern is `${message}` inside a template literal assigned to innerHTML
|
||||
# After the fix, message must only appear via textContent assignment
|
||||
assert "textContent = message" in html or "textContent=message" in html, (
|
||||
"mobile.html still uses innerHTML + ${message} interpolation — XSS vulnerability"
|
||||
)
|
||||
|
||||
|
||||
def test_M702_mobile_chat_user_input_not_in_innerhtml_template_literal():
|
||||
"""${message} must not appear inside a backtick string that is assigned to innerHTML."""
|
||||
html = _mobile_html()
|
||||
# Find all innerHTML += `...` blocks and verify none contain ${message}
|
||||
blocks = re.findall(r"innerHTML\s*\+=?\s*`([^`]*)`", html, re.DOTALL)
|
||||
for block in blocks:
|
||||
assert "${message}" not in block, (
|
||||
"innerHTML template literal still contains ${message} — XSS vulnerability"
|
||||
)
|
||||
|
||||
|
||||
def test_M703_swarm_live_agent_name_not_interpolated_in_innerhtml():
|
||||
"""swarm_live.html must not put ${agent.name} inside innerHTML template literals."""
|
||||
html = _swarm_live_html()
|
||||
blocks = re.findall(r"innerHTML\s*=\s*agents\.map\([^;]+\)\.join\([^)]*\)", html, re.DOTALL)
|
||||
assert len(blocks) == 0, (
|
||||
"swarm_live.html still uses innerHTML=agents.map(…) with interpolated agent data — XSS vulnerability"
|
||||
)
|
||||
|
||||
|
||||
def test_M704_swarm_live_uses_textcontent_for_agent_data():
|
||||
"""swarm_live.html must use textContent (not innerHTML) to set agent name/description."""
|
||||
html = _swarm_live_html()
|
||||
assert "textContent" in html, (
|
||||
"swarm_live.html does not use textContent — agent data may be raw-interpolated into DOM"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user