mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
9be155b97a
* Quality overhaul: code tooling, CI/CD, architecture diagrams, UI redesign, and legacy cleanup - Add ruff (lint+format) and mypy (strict) with zero errors across 37 source files - Add GitHub Actions CI (lint, typecheck, test matrix 3.11/3.12/3.13) and PyPI publish workflow - Create 12 PlantUML architecture diagrams with PNG renders covering all subsystems - Refresh README and docs with badges, diagram links, and current descriptions - Refactor test_server_live.py with mock streaming helpers for deterministic CI testing - Update dependencies to current versions (openai>=2.24, httpx>=0.28, redis>=7.2) Console dashboard: - Move state indicators from top cards to fixed bottom status bar with cluster metrics - Replace flat 50-node list with hostname-prefix grouped nodes (expand/collapse, up to 1000) - Apply "Instrument Panel" visual redesign: IBM Plex Mono + Outfit fonts, warm amber accent, LED glow state indicators, deep charcoal surfaces, WCAG AA contrast compliance - Add render cache, stale indicator, active filter highlight, loading states Server web UI: - Apply matching Instrument Panel aesthetic for visual consistency with console - Fix branding (pcode → turnstone), extract inline styles to CSS classes - Rename pcode localStorage keys and history state to turnstone Legacy cleanup: - Remove persona-model-specific --persona flag and /persona slash command - Remove model_identity from chat_template_kwargs (vLLM-specific mechanism) - Refactor plan agent to use standard developer message instead of model_identity - Remove dead code (unused date/has_tools variables, noqa suppressions) * Fix CI typecheck: add mypy overrides for optional sympy/numpy imports The math sandbox optionally imports sympy and numpy at runtime (try/except ImportError). In CI these packages are not installed, so mypy raises import-not-found rather than import-untyped. Add mypy overrides to ignore missing imports for these optional dependencies. * Fix Copilot review findings: ARIA role, status bar cache, and pulse opacity - Change #node-table from role="tree" to role="list" and group elements from role="treeitem" to role="listitem" (proper ARIA semantics) - Include currentView and currentFilter.state in renderStatusBar cache key so active pill highlight updates when switching views - Align pulse animation to 0.35 opacity (already applied in CSS)
125 lines
3.5 KiB
Plaintext
125 lines
3.5 KiB
Plaintext
@startuml
|
|
!theme plain
|
|
title Turnstone — Console Dashboard Data Collection
|
|
|
|
skinparam sequenceArrowThickness 1.5
|
|
|
|
participant "Browser" as Browser
|
|
participant "Console\nHTTP Server" as Server
|
|
participant "ClusterCollector" as CC
|
|
collections "Redis" as Redis
|
|
participant "Node-A\n(real server)" as NodeA
|
|
participant "Node-B\n(sim node)" as NodeB
|
|
|
|
== Thread 1: Cluster Event Subscriber (real-time) ==
|
|
|
|
CC -> Redis : SUBSCRIBE turnstone:events:cluster
|
|
activate CC #E1BEE7
|
|
|
|
Redis --> CC : ClusterStateEvent\n{ws_id, state:"thinking",\nnode_id:"nodeA", tokens:500,\ncontext_ratio:0.05}
|
|
CC -> CC : Update NodeSnapshot["nodeA"]\n.workstreams["ws123"].state = "thinking"
|
|
CC -> CC : _fanout(event) → all SSE listeners
|
|
|
|
Redis --> CC : {"type":"ws_created",\nws_id:"ws456", name:"task-1",\nnode_id:"sim-0003"}
|
|
CC -> CC : Add workstream to\nNodeSnapshot["sim-0003"]
|
|
CC -> CC : _fanout(event)
|
|
|
|
Redis --> CC : ClusterStateEvent\n{ws_id:"ws456", state:"idle"}
|
|
CC -> CC : Update workstream state
|
|
|
|
note right of CC
|
|
Handles: cluster_state,
|
|
ws_created, ws_closed, ws_rename
|
|
|
|
Thread runs continuously.
|
|
All updates are thread-safe
|
|
via threading.Lock.
|
|
end note
|
|
|
|
deactivate CC
|
|
|
|
== Thread 2: Node Discovery (every 15s) ==
|
|
|
|
CC -> Redis : SCAN 0 MATCH turnstone:node:*
|
|
activate CC #B2EBF2
|
|
Redis --> CC : [turnstone:node:nodeA, turnstone:node:sim-0003, ...]
|
|
|
|
loop for each discovered key
|
|
CC -> Redis : GET turnstone:node:{id}
|
|
Redis --> CC : JSON: {server_url, started, max_ws, sim:true/false}
|
|
end
|
|
|
|
CC -> CC : Create new NodeSnapshot\nfor newly discovered nodes
|
|
CC -> CC : Remove NodeSnapshot\nfor disappeared nodes
|
|
|
|
CC -> CC : _fanout({type: "node_joined", ...})\n_fanout({type: "node_lost", ...})
|
|
|
|
deactivate CC
|
|
|
|
== Thread 3: HTTP Polling (every 10s, real nodes only) ==
|
|
|
|
CC -> CC : Filter nodes where\nserver_url.startswith("http")
|
|
activate CC #C8E6C9
|
|
|
|
note right of CC
|
|
sim:// nodes are SKIPPED.
|
|
Their data comes exclusively
|
|
from the cluster event channel.
|
|
end note
|
|
|
|
CC -> NodeA : GET /api/dashboard
|
|
activate NodeA
|
|
NodeA --> CC : {workstreams: [...],\naggregate: {total_tokens, ...}}
|
|
deactivate NodeA
|
|
|
|
CC -> NodeA : GET /health
|
|
activate NodeA
|
|
NodeA --> CC : {status:"ok", model:"...",\nworkstreams:{total, idle, ...}}
|
|
deactivate NodeA
|
|
|
|
CC -> CC : Replace NodeSnapshot["nodeA"]\n.workstreams, .health, .aggregate
|
|
|
|
CC -x NodeB : (SKIPPED: sim:// URL)
|
|
|
|
deactivate CC
|
|
|
|
== Browser SSE Stream ==
|
|
|
|
Browser -> Server : GET /api/cluster/events
|
|
activate Server
|
|
|
|
Server -> CC : register_listener(queue)
|
|
note right : Per-client queue.Queue(maxsize=500)
|
|
|
|
loop continuous
|
|
CC -> Server : event via listener queue\n(from any of the 3 threads)
|
|
Server -> Browser : data: {"type":"cluster_state",...}\n\n
|
|
end
|
|
|
|
alt timeout (5s no events)
|
|
Server -> Browser : : keepalive\n\n
|
|
end
|
|
|
|
Browser -> Server : connection closed
|
|
Server -> CC : unregister_listener(queue)
|
|
deactivate Server
|
|
|
|
== Browser REST Requests ==
|
|
|
|
Browser -> Server : GET /api/cluster/overview
|
|
Server -> CC : get_overview()
|
|
CC --> Server : {nodes: 10, workstreams: 47,\nstates: {running:5, thinking:3, ...},\naggregate: {total_tokens: 50000}}
|
|
Server --> Browser : JSON response
|
|
|
|
Browser -> Server : GET /api/cluster/nodes?sort=activity
|
|
Server -> CC : get_nodes(sort_by="activity")
|
|
CC --> Server : {nodes: [...], total: 10}
|
|
Server --> Browser : JSON response
|
|
|
|
Browser -> Server : GET /api/cluster/workstreams\n?state=running&node=sim-0003
|
|
Server -> CC : get_workstreams(state="running",\nnode="sim-0003")
|
|
CC --> Server : {workstreams: [...], total: 5,\npage: 1, per_page: 50, pages: 1}
|
|
Server --> Browser : JSON response
|
|
|
|
@enduml
|