devsquad 3.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- skills/test/handler.py +78 -0
scripts/dashboard.py
ADDED
|
@@ -0,0 +1,1088 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
DevSquad V3.6.0-C Dashboard (Streamlit Production-Grade)
|
|
5
|
+
|
|
6
|
+
Interactive web dashboard for lifecycle visualization, monitoring,
|
|
7
|
+
and task dispatch.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Real-time lifecycle phase status
|
|
11
|
+
- CLI command mapping visualization
|
|
12
|
+
- Gate status monitoring
|
|
13
|
+
- Performance metrics (real API data)
|
|
14
|
+
- Task dispatch interface
|
|
15
|
+
- Admin pages (user management & system config)
|
|
16
|
+
- Auto-refresh with countdown timer
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
streamlit run scripts/dashboard.py
|
|
20
|
+
|
|
21
|
+
Requirements:
|
|
22
|
+
streamlit>=1.28.0
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import sys
|
|
26
|
+
import os
|
|
27
|
+
import time
|
|
28
|
+
import requests
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
31
|
+
|
|
32
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
33
|
+
|
|
34
|
+
import streamlit as st
|
|
35
|
+
|
|
36
|
+
from scripts.auth import AuthManager, User, UserRole
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DashboardConfig:
|
|
40
|
+
"""Dashboard configuration and styling."""
|
|
41
|
+
|
|
42
|
+
PAGE_TITLE = "🔄 DevSquad Lifecycle Dashboard"
|
|
43
|
+
PAGE_ICON = "🚀"
|
|
44
|
+
LAYOUT = "wide"
|
|
45
|
+
|
|
46
|
+
COLOR_SCHEME = {
|
|
47
|
+
"primary": "#4A90D9",
|
|
48
|
+
"success": "#2ca02c",
|
|
49
|
+
"warning": "#ff7f0e",
|
|
50
|
+
"danger": "#d62728",
|
|
51
|
+
"info": "#17becf",
|
|
52
|
+
"background": "#ffffff",
|
|
53
|
+
"text": "#333333",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
PHASE_COLORS = {
|
|
57
|
+
"pending": "#95a5a6",
|
|
58
|
+
"running": "#3498db",
|
|
59
|
+
"completed": "#27ae60",
|
|
60
|
+
"failed": "#e74c3c",
|
|
61
|
+
"skipped": "#f39c12",
|
|
62
|
+
"blocked": "#c0392b",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
CORE_ROLES = [
|
|
66
|
+
"architect",
|
|
67
|
+
"product-manager",
|
|
68
|
+
"security",
|
|
69
|
+
"tester",
|
|
70
|
+
"solo-coder",
|
|
71
|
+
"devops",
|
|
72
|
+
"ui-designer",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
ROLE_ICONS = {
|
|
76
|
+
"architect": "🏗️",
|
|
77
|
+
"product-manager": "📋",
|
|
78
|
+
"security": "🔒",
|
|
79
|
+
"tester": "🧪",
|
|
80
|
+
"solo-coder": "💻",
|
|
81
|
+
"devops": "⚙️",
|
|
82
|
+
"ui-designer": "🎨",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
ROLE_NAMES = {
|
|
86
|
+
"architect": "架构师",
|
|
87
|
+
"product-manager": "产品经理",
|
|
88
|
+
"security": "安全专家",
|
|
89
|
+
"tester": "测试专家",
|
|
90
|
+
"solo-coder": "开发者",
|
|
91
|
+
"devops": "运维工程师",
|
|
92
|
+
"ui-designer": "UI设计师",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@st.cache_resource
|
|
97
|
+
def load_lifecycle_protocol():
|
|
98
|
+
"""Load and cache the lifecycle protocol."""
|
|
99
|
+
try:
|
|
100
|
+
from scripts.collaboration.lifecycle_protocol import (
|
|
101
|
+
get_shared_protocol,
|
|
102
|
+
VIEW_MAPPINGS,
|
|
103
|
+
FULL_LIFECYCLE_PHASES,
|
|
104
|
+
)
|
|
105
|
+
return {
|
|
106
|
+
"protocol": get_shared_protocol(),
|
|
107
|
+
"mappings": VIEW_MAPPINGS,
|
|
108
|
+
"phases": FULL_LIFECYCLE_PHASES,
|
|
109
|
+
}
|
|
110
|
+
except Exception as e:
|
|
111
|
+
st.error(f"Failed to load lifecycle protocol: {e}")
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@st.cache_resource
|
|
116
|
+
def get_dispatcher():
|
|
117
|
+
"""Initialize and cache the MultiAgentDispatcher."""
|
|
118
|
+
try:
|
|
119
|
+
from scripts.collaboration.dispatcher import MultiAgentDispatcher
|
|
120
|
+
return MultiAgentDispatcher(
|
|
121
|
+
enable_warmup=True,
|
|
122
|
+
enable_compression=True,
|
|
123
|
+
enable_permission=True,
|
|
124
|
+
enable_memory=True,
|
|
125
|
+
enable_skillify=True,
|
|
126
|
+
lang="auto",
|
|
127
|
+
)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
st.error(f"Failed to initialize dispatcher: {e}")
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def set_page_config():
|
|
134
|
+
"""Configure Streamlit page settings."""
|
|
135
|
+
st.set_page_config(
|
|
136
|
+
page_title=DashboardConfig.PAGE_TITLE,
|
|
137
|
+
page_icon=DashboardConfig.PAGE_ICON,
|
|
138
|
+
layout=DashboardConfig.LAYOUT,
|
|
139
|
+
initial_sidebar_state="expanded",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def apply_custom_css():
|
|
144
|
+
"""Apply custom CSS for production-grade visual appearance."""
|
|
145
|
+
st.markdown("""
|
|
146
|
+
<style>
|
|
147
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
|
148
|
+
|
|
149
|
+
* {
|
|
150
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.main-header {
|
|
154
|
+
font-size: 2.5rem;
|
|
155
|
+
font-weight: 700;
|
|
156
|
+
color: #4A90D9;
|
|
157
|
+
text-align: center;
|
|
158
|
+
padding: 1rem 0;
|
|
159
|
+
background: linear-gradient(135deg, #4A90D9 0%, #357ABD 100%);
|
|
160
|
+
-webkit-background-clip: text;
|
|
161
|
+
-webkit-text-fill-color: transparent;
|
|
162
|
+
background-clip: text;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.metric-card {
|
|
166
|
+
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
|
167
|
+
border-radius: 12px;
|
|
168
|
+
padding: 1.5rem;
|
|
169
|
+
margin: 0.5rem 0;
|
|
170
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
|
|
171
|
+
border: 1px solid #e9ecef;
|
|
172
|
+
transition: all 0.3s ease;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.metric-card:hover {
|
|
176
|
+
transform: translateY(-2px);
|
|
177
|
+
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.12);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.phase-card {
|
|
181
|
+
background-color: white;
|
|
182
|
+
border-radius: 10px;
|
|
183
|
+
padding: 1.25rem;
|
|
184
|
+
margin: 0.5rem 0;
|
|
185
|
+
border-left: 4px solid #4A90D9;
|
|
186
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06);
|
|
187
|
+
transition: all 0.3s ease;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.phase-card:hover {
|
|
191
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.status-badge {
|
|
195
|
+
display: inline-block;
|
|
196
|
+
padding: 0.35rem 0.85rem;
|
|
197
|
+
border-radius: 9999px;
|
|
198
|
+
font-size: 0.875rem;
|
|
199
|
+
font-weight: 600;
|
|
200
|
+
letter-spacing: 0.02em;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.status-success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
|
204
|
+
.status-warning { background-color: #fff3cd; color: #856404; border: 1px solid #ffeaa7; }
|
|
205
|
+
.status-danger { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
|
206
|
+
.status-info { background-color: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
|
|
207
|
+
.status-secondary { background-color: #e2e3e5; color: #383d41; border: 1px solid #d6d8db; }
|
|
208
|
+
|
|
209
|
+
.task-input textarea {
|
|
210
|
+
border: 2px solid #4A90D9 !important;
|
|
211
|
+
border-radius: 8px !important;
|
|
212
|
+
font-family: 'Monaco', 'Menlo', monospace !important;
|
|
213
|
+
font-size: 14px !important;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.primary-btn {
|
|
217
|
+
background: linear-gradient(135deg, #4A90D9 0%, #357ABD 100%) !important;
|
|
218
|
+
color: white !important;
|
|
219
|
+
border: none !important;
|
|
220
|
+
border-radius: 8px !important;
|
|
221
|
+
padding: 0.65rem 1.5rem !important;
|
|
222
|
+
font-weight: 600 !important;
|
|
223
|
+
transition: all 0.3s ease !important;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.primary-btn:hover {
|
|
227
|
+
transform: translateY(-1px) !important;
|
|
228
|
+
box-shadow: 0 4px 12px rgba(74, 144, 217, 0.35) !important;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
[data-testid="stMetric"] {
|
|
232
|
+
background-color: white;
|
|
233
|
+
padding: 1rem;
|
|
234
|
+
border-radius: 10px;
|
|
235
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.loading-spinner {
|
|
239
|
+
display: inline-block;
|
|
240
|
+
width: 20px;
|
|
241
|
+
height: 20px;
|
|
242
|
+
border: 3px solid rgba(74, 144, 217, 0.3);
|
|
243
|
+
border-top-color: #4A90D9;
|
|
244
|
+
border-radius: 50%;
|
|
245
|
+
animation: spin 1s linear infinite;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@keyframes spin {
|
|
249
|
+
to { transform: rotate(360deg); }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.countdown-timer {
|
|
253
|
+
font-size: 0.9rem;
|
|
254
|
+
color: #666;
|
|
255
|
+
font-weight: 500;
|
|
256
|
+
padding: 0.5rem 1rem;
|
|
257
|
+
background: #f8f9fa;
|
|
258
|
+
border-radius: 20px;
|
|
259
|
+
display: inline-block;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
div[data-testid="stExpander"] {
|
|
263
|
+
border: 1px solid #dee2e6;
|
|
264
|
+
border-radius: 8px;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
h2, h3 {
|
|
268
|
+
color: #2c3e50 !important;
|
|
269
|
+
font-weight: 600 !important;
|
|
270
|
+
}
|
|
271
|
+
</style>
|
|
272
|
+
""", unsafe_allow_html=True)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def render_header(current_user: Optional[User] = None):
|
|
276
|
+
"""Render the main dashboard header with enhanced styling."""
|
|
277
|
+
st.markdown('<div class="main-header">🚀 DevSquad Lifecycle Dashboard</div>', unsafe_allow_html=True)
|
|
278
|
+
|
|
279
|
+
col1, col2, col3, col4 = st.columns([2, 1, 1, 1])
|
|
280
|
+
with col1:
|
|
281
|
+
st.markdown("**Plan C Architecture** | CLI View Layer over 11-Phase Lifecycle")
|
|
282
|
+
if current_user:
|
|
283
|
+
role_badge_class = "status-info" if current_user.role == UserRole.ADMIN else "status-success"
|
|
284
|
+
st.markdown(
|
|
285
|
+
f'<span class="status-badge {role_badge_class}">👤 {current_user.name} ({current_user.role.value})</span>',
|
|
286
|
+
unsafe_allow_html=True
|
|
287
|
+
)
|
|
288
|
+
with col2:
|
|
289
|
+
st.markdown(f"**Last Updated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
290
|
+
with col3:
|
|
291
|
+
if st.button("🔄 Refresh", key="refresh_header"):
|
|
292
|
+
st.rerun()
|
|
293
|
+
with col4:
|
|
294
|
+
if st.session_state.get('auto_refresh', False):
|
|
295
|
+
remaining = st.session_state.get('refresh_countdown', 30)
|
|
296
|
+
st.markdown(
|
|
297
|
+
f'<div class="countdown-timer">⏱️ Auto-refresh in {remaining}s</div>',
|
|
298
|
+
unsafe_allow_html=True
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def render_metrics_overview(protocol_data):
|
|
303
|
+
"""Render key metrics overview cards with enhanced styling."""
|
|
304
|
+
if not protocol_data:
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
protocol = protocol_data["protocol"]
|
|
308
|
+
phases = protocol_data["phases"]
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
status = protocol.get_status()
|
|
312
|
+
|
|
313
|
+
total_phases = len(phases)
|
|
314
|
+
completed = len(status.get("completed_phases", []))
|
|
315
|
+
running = len(status.get("running_phases", []))
|
|
316
|
+
failed = len(status.get("failed_phases", []))
|
|
317
|
+
|
|
318
|
+
completion_rate = (completed / total_phases * 100) if total_phases > 0 else 0
|
|
319
|
+
|
|
320
|
+
col1, col2, col3, col4, col5 = st.columns(5)
|
|
321
|
+
|
|
322
|
+
with col1:
|
|
323
|
+
st.metric(
|
|
324
|
+
label="Total Phases",
|
|
325
|
+
value=f"{total_phases}",
|
|
326
|
+
delta=None,
|
|
327
|
+
help="Total number of lifecycle phases"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
with col2:
|
|
331
|
+
delta_color = "normal" if completion_rate >= 50 else "inverse"
|
|
332
|
+
st.metric(
|
|
333
|
+
label="Completed",
|
|
334
|
+
value=f"{completed}",
|
|
335
|
+
delta=f"{completion_rate:.1f}%",
|
|
336
|
+
delta_color=delta_color,
|
|
337
|
+
help="Phases successfully completed"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
with col3:
|
|
341
|
+
st.metric(
|
|
342
|
+
label="Running",
|
|
343
|
+
value=f"{running}",
|
|
344
|
+
delta_color="off",
|
|
345
|
+
help="Phases currently executing"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
with col4:
|
|
349
|
+
st.metric(
|
|
350
|
+
label="Failed",
|
|
351
|
+
value=f"{failed}",
|
|
352
|
+
delta_color="inverse" if failed > 0 else "normal",
|
|
353
|
+
help="Phases that failed execution"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
with col5:
|
|
357
|
+
progress_color = "#27ae60" if completion_rate >= 70 else "#ff7f0e" if completion_rate >= 40 else "#d62728"
|
|
358
|
+
st.metric(
|
|
359
|
+
label="Progress",
|
|
360
|
+
value=f"{completion_rate:.1f}%",
|
|
361
|
+
delta=None,
|
|
362
|
+
help="Overall project progress",
|
|
363
|
+
delta_color="normal"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
except Exception as e:
|
|
367
|
+
st.warning(f"Could not load metrics: {e}")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def render_phase_timeline(protocol_data):
|
|
371
|
+
"""Render interactive phase timeline visualization with enhanced cards."""
|
|
372
|
+
if not protocol_data:
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
st.subheader("📋 Phase Timeline")
|
|
376
|
+
|
|
377
|
+
phases = protocol_data["phases"]
|
|
378
|
+
protocol = protocol_data["protocol"]
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
status = protocol.get_status()
|
|
382
|
+
completed_phases = set(status.get("completed_phases", []))
|
|
383
|
+
running_phases = set(status.get("running_phases", []))
|
|
384
|
+
failed_phases = set(status.get("failed_phases", []))
|
|
385
|
+
|
|
386
|
+
for idx, phase in enumerate(phases, 1):
|
|
387
|
+
phase_id = phase.phase_id
|
|
388
|
+
|
|
389
|
+
if phase_id in completed_phases:
|
|
390
|
+
status_icon = "✅"
|
|
391
|
+
status_text = "Completed"
|
|
392
|
+
badge_class = "status-success"
|
|
393
|
+
elif phase_id in running_phases:
|
|
394
|
+
status_icon = "🔄"
|
|
395
|
+
status_text = "Running"
|
|
396
|
+
badge_class = "status-info"
|
|
397
|
+
elif phase_id in failed_phases:
|
|
398
|
+
status_icon = "❌"
|
|
399
|
+
status_text = "Failed"
|
|
400
|
+
badge_class = "status-danger"
|
|
401
|
+
else:
|
|
402
|
+
status_icon = "⏳"
|
|
403
|
+
status_text = "Pending"
|
|
404
|
+
badge_class = "status-secondary"
|
|
405
|
+
|
|
406
|
+
with st.container():
|
|
407
|
+
st.markdown('<div class="phase-card">', unsafe_allow_html=True)
|
|
408
|
+
col1, col2, col3, col4 = st.columns([1, 3, 2, 2])
|
|
409
|
+
|
|
410
|
+
with col1:
|
|
411
|
+
st.markdown(f"**P{idx:02d}**")
|
|
412
|
+
|
|
413
|
+
with col2:
|
|
414
|
+
st.markdown(f"**{phase.name}**")
|
|
415
|
+
desc = phase.description[:80] + ("..." if len(phase.description) > 80 else "")
|
|
416
|
+
st.caption(desc)
|
|
417
|
+
|
|
418
|
+
with col3:
|
|
419
|
+
st.code(phase.role_id)
|
|
420
|
+
|
|
421
|
+
with col4:
|
|
422
|
+
st.markdown(
|
|
423
|
+
f'<span class="status-badge {badge_class}">{status_icon} {status_text}</span>',
|
|
424
|
+
unsafe_allow_html=True
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
st.markdown('</div>', unsafe_allow_html=True)
|
|
428
|
+
st.divider()
|
|
429
|
+
|
|
430
|
+
except Exception as e:
|
|
431
|
+
st.error(f"Error rendering timeline: {e}")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def render_cli_mapping_table(protocol_data):
|
|
435
|
+
"""Render CLI command to phase mapping table."""
|
|
436
|
+
if not protocol_data:
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
st.subheader("🔗 CLI Command Mapping (Plan C View Layer)")
|
|
440
|
+
|
|
441
|
+
mappings = protocol_data["mappings"]
|
|
442
|
+
protocol = protocol_data["protocol"]
|
|
443
|
+
|
|
444
|
+
mapping_data = []
|
|
445
|
+
for cmd_name, mapping in mappings.items():
|
|
446
|
+
try:
|
|
447
|
+
resolved_phases = protocol.resolve_command_to_phases(cmd_name)
|
|
448
|
+
phase_ids = [p.phase_id for p in resolved_phases] if resolved_phases else mapping.phases
|
|
449
|
+
|
|
450
|
+
mapping_data.append({
|
|
451
|
+
"Command": cmd_name.upper(),
|
|
452
|
+
"Phases": ", ".join(phase_ids),
|
|
453
|
+
"Phase Count": len(phase_ids),
|
|
454
|
+
"Mode": mapping.mode or "N/A",
|
|
455
|
+
"Gate": mapping.gate or "None",
|
|
456
|
+
})
|
|
457
|
+
except Exception:
|
|
458
|
+
mapping_data.append({
|
|
459
|
+
"Command": cmd_name.upper(),
|
|
460
|
+
"Phases": ", ".join(mapping.phases),
|
|
461
|
+
"Phase Count": len(mapping.phases),
|
|
462
|
+
"Mode": mapping.mode or "N/A",
|
|
463
|
+
"Gate": mapping.gate or "None",
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
if mapping_data:
|
|
467
|
+
st.dataframe(
|
|
468
|
+
mapping_data,
|
|
469
|
+
use_container_width=True,
|
|
470
|
+
hide_index=True,
|
|
471
|
+
column_config={
|
|
472
|
+
"Command": st.column_config.TextColumn("Command", width="medium"),
|
|
473
|
+
"Phases": st.column_config.TextColumn("Mapped Phases", width="large"),
|
|
474
|
+
"Phase Count": st.column_config.NumberColumn("Count", width="small"),
|
|
475
|
+
"Mode": st.column_config.TextColumn("Mode", width="small"),
|
|
476
|
+
"Gate": st.column_config.TextColumn("Gate", width="medium"),
|
|
477
|
+
}
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def render_gate_status_panel(protocol_data):
|
|
482
|
+
"""Render gate status monitoring panel."""
|
|
483
|
+
if not protocol_data:
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
st.subheader("🚧 Gate Status Monitor")
|
|
487
|
+
|
|
488
|
+
protocol = protocol_data["protocol"]
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
gate_results = {}
|
|
492
|
+
|
|
493
|
+
test_commands = ["spec", "plan", "build", "test", "review", "ship"]
|
|
494
|
+
for cmd in test_commands:
|
|
495
|
+
try:
|
|
496
|
+
result = protocol.check_command_gate(cmd)
|
|
497
|
+
gate_results[cmd] = result
|
|
498
|
+
except Exception:
|
|
499
|
+
pass
|
|
500
|
+
|
|
501
|
+
if gate_results:
|
|
502
|
+
for cmd, result in gate_results.items():
|
|
503
|
+
passed = getattr(result, 'passed', False)
|
|
504
|
+
verdict = getattr(result, 'verdict', 'UNKNOWN')
|
|
505
|
+
|
|
506
|
+
col1, col2, col3 = st.columns([2, 2, 3])
|
|
507
|
+
|
|
508
|
+
with col1:
|
|
509
|
+
st.markdown(f"**{cmd.upper()}**")
|
|
510
|
+
|
|
511
|
+
with col2:
|
|
512
|
+
if passed:
|
|
513
|
+
st.success(f"✅ {verdict}")
|
|
514
|
+
else:
|
|
515
|
+
st.error(f"❌ {verdict}")
|
|
516
|
+
|
|
517
|
+
with col3:
|
|
518
|
+
red_flags = getattr(result, 'red_flags', [])
|
|
519
|
+
missing = getattr(result, 'missing_evidence', [])
|
|
520
|
+
|
|
521
|
+
flags_text = f"🚩 {len(red_flags)} flags" if red_flags else "No flags"
|
|
522
|
+
missing_text = f"📋 {len(missing)} missing" if missing else "Complete"
|
|
523
|
+
|
|
524
|
+
st.markdown(f"{flags_text} | {missing_text}")
|
|
525
|
+
|
|
526
|
+
st.divider()
|
|
527
|
+
|
|
528
|
+
except Exception as e:
|
|
529
|
+
st.warning(f"Could not load gate status: {e}")
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def fetch_api_metrics():
|
|
533
|
+
"""
|
|
534
|
+
Fetch real metrics from API server.
|
|
535
|
+
Returns dict if successful, None if API unreachable.
|
|
536
|
+
"""
|
|
537
|
+
try:
|
|
538
|
+
response = requests.get(
|
|
539
|
+
"http://localhost:8000/api/v1/metrics/current",
|
|
540
|
+
timeout=3
|
|
541
|
+
)
|
|
542
|
+
if response.status_code == 200:
|
|
543
|
+
return response.json()
|
|
544
|
+
return None
|
|
545
|
+
except (requests.ConnectionError, requests.Timeout):
|
|
546
|
+
return None
|
|
547
|
+
except Exception:
|
|
548
|
+
return None
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def render_performance_panel():
|
|
552
|
+
"""Render performance metrics panel with real API data."""
|
|
553
|
+
st.subheader("📊 System Performance")
|
|
554
|
+
|
|
555
|
+
with st.spinner("Connecting to API Server..."):
|
|
556
|
+
metrics_data = fetch_api_metrics()
|
|
557
|
+
|
|
558
|
+
if metrics_data:
|
|
559
|
+
st.success("✅ Connected to API Server")
|
|
560
|
+
|
|
561
|
+
col1, col2 = st.columns(2)
|
|
562
|
+
|
|
563
|
+
with col1:
|
|
564
|
+
st.markdown("**Response Time Metrics**")
|
|
565
|
+
real_metrics = [
|
|
566
|
+
{"Metric": "Avg Response Time", "Value": f"{metrics_data.get('avg_response_time_ms', 'N/A')}ms"},
|
|
567
|
+
{"Metric": "P95 Latency", "Value": f"{metrics_data.get('p95_latency_ms', 'N/A')}ms"},
|
|
568
|
+
{"Metric": "Success Rate", "Value": f"{metrics_data.get('success_rate', 'N/A')}%"},
|
|
569
|
+
{"Metric": "Throughput", "Value": f"{metrics_data.get('throughput', 'N/A')} req/s"},
|
|
570
|
+
]
|
|
571
|
+
st.dataframe(real_metrics, use_container_width=True, hide_index=True)
|
|
572
|
+
|
|
573
|
+
with col2:
|
|
574
|
+
st.markdown("**Resource Utilization**")
|
|
575
|
+
cpu_usage = metrics_data.get('cpu_usage_percent', 0)
|
|
576
|
+
mem_usage = metrics_data.get('memory_usage_percent', 0)
|
|
577
|
+
|
|
578
|
+
st.progress(cpu_usage / 100 if cpu_usage > 0 else 0, text=f"CPU Usage: {cpu_usage:.1f}%")
|
|
579
|
+
st.progress(mem_usage / 100 if mem_usage > 0 else 0, text=f"Memory Usage: {mem_usage:.1f}%")
|
|
580
|
+
|
|
581
|
+
st.caption("*Real-time data from API Server*")
|
|
582
|
+
|
|
583
|
+
col3, col4 = st.columns(2)
|
|
584
|
+
with col3:
|
|
585
|
+
st.metric(
|
|
586
|
+
label="Completion Rate",
|
|
587
|
+
value=f"{metrics_data.get('completion_rate', 0)}%",
|
|
588
|
+
help="Lifecycle phase completion rate"
|
|
589
|
+
)
|
|
590
|
+
with col4:
|
|
591
|
+
st.metric(
|
|
592
|
+
label="Active Phases",
|
|
593
|
+
value=metrics_data.get('running_phases', 0),
|
|
594
|
+
help="Currently running phases"
|
|
595
|
+
)
|
|
596
|
+
else:
|
|
597
|
+
st.error("❌ **API Server 未运行**")
|
|
598
|
+
st.info("""
|
|
599
|
+
**无法连接到 localhost:8000**
|
|
600
|
+
|
|
601
|
+
请确保 DevSquad API Server 正在运行:
|
|
602
|
+
```bash
|
|
603
|
+
python scripts/api_server.py
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
或者使用以下命令启动:
|
|
607
|
+
```bash
|
|
608
|
+
uvicorn scripts.api_server:app --host 0.0.0.0 --port 8000
|
|
609
|
+
```
|
|
610
|
+
""")
|
|
611
|
+
|
|
612
|
+
st.markdown("**模拟数据预览(仅用于演示)**")
|
|
613
|
+
with st.expander("查看示例数据格式"):
|
|
614
|
+
sample_data = [
|
|
615
|
+
{"Metric": "Avg Response Time", "Value": "~250ms (expected)"},
|
|
616
|
+
{"Metric": "P95 Latency", "Value": "~1200ms (expected)"},
|
|
617
|
+
{"Metric": "Success Rate", "Value": "~98.5% (expected)"},
|
|
618
|
+
{"Metric": "Throughput", "Value": "~120 req/s (expected)"},
|
|
619
|
+
]
|
|
620
|
+
st.dataframe(sample_data, use_container_width=True, hide_index=True)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def render_task_dispatch_page(dispatcher):
|
|
624
|
+
"""Render the Task Dispatch page with full functionality."""
|
|
625
|
+
st.header("🎯 Task Dispatch")
|
|
626
|
+
st.markdown("---")
|
|
627
|
+
|
|
628
|
+
col_input, col_config = st.columns([3, 2])
|
|
629
|
+
|
|
630
|
+
with col_input:
|
|
631
|
+
st.markdown("**Task Description**")
|
|
632
|
+
task_description = st.text_area(
|
|
633
|
+
"Enter your task description...",
|
|
634
|
+
height=150,
|
|
635
|
+
placeholder="Example: Design a RESTful API for user authentication with JWT tokens...",
|
|
636
|
+
help="Describe the task you want the multi-agent team to work on"
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
with col_config:
|
|
640
|
+
st.markdown("**Configuration**")
|
|
641
|
+
|
|
642
|
+
selected_roles = st.multiselect(
|
|
643
|
+
"Select Roles (leave empty for auto-match)",
|
|
644
|
+
options=DashboardConfig.CORE_ROLES,
|
|
645
|
+
format_func=lambda x: f"{DashboardConfig.ROLE_ICONS.get(x, '🤖')} {DashboardConfig.ROLE_NAMES.get(x, x)}",
|
|
646
|
+
default=[],
|
|
647
|
+
help="Choose specific roles or let AI auto-match based on task"
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
execution_mode = st.selectbox(
|
|
651
|
+
"Execution Mode",
|
|
652
|
+
options=["auto", "parallel", "sequential", "consensus"],
|
|
653
|
+
format_func=lambda x: {
|
|
654
|
+
"auto": "🤖 Auto (Recommended)",
|
|
655
|
+
"parallel": "⚡ Parallel (Fastest)",
|
|
656
|
+
"sequential": "📋 Sequential (Ordered)",
|
|
657
|
+
"consensus": "🗳️ Consensus (Thorough)"
|
|
658
|
+
}.get(x, x),
|
|
659
|
+
index=0,
|
|
660
|
+
help="How agents should collaborate on this task"
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
language = st.selectbox(
|
|
664
|
+
"Output Language",
|
|
665
|
+
options=["auto", "zh", "en", "ja"],
|
|
666
|
+
format_func=lambda x: {
|
|
667
|
+
"auto": "🌐 Auto-detect",
|
|
668
|
+
"zh": "🇨🇳 中文",
|
|
669
|
+
"en": "🇺🇸 English",
|
|
670
|
+
"ja": "🇯🇵 日本語"
|
|
671
|
+
}.get(x, x),
|
|
672
|
+
index=0,
|
|
673
|
+
help="Language for output and reports"
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
backend = st.selectbox(
|
|
677
|
+
"LLM Backend",
|
|
678
|
+
options=["mock", "openai", "anthropic"],
|
|
679
|
+
format_func=lambda x: {
|
|
680
|
+
"mock": "🎭 Mock (Demo)",
|
|
681
|
+
"openai": "🟢 OpenAI GPT",
|
|
682
|
+
"anthropic": "🟣 Anthropic Claude"
|
|
683
|
+
}.get(x, x),
|
|
684
|
+
index=0,
|
|
685
|
+
help="AI backend to use for agent responses"
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
st.markdown("---")
|
|
689
|
+
|
|
690
|
+
col_submit, col_clear = st.columns([1, 4])
|
|
691
|
+
|
|
692
|
+
with col_submit:
|
|
693
|
+
submit_disabled = not task_description.strip()
|
|
694
|
+
if st.button(
|
|
695
|
+
"🚀 Submit Task",
|
|
696
|
+
type="primary",
|
|
697
|
+
disabled=submit_disabled,
|
|
698
|
+
use_container_width=True,
|
|
699
|
+
help="Dispatch task to multi-agent system"
|
|
700
|
+
):
|
|
701
|
+
if not dispatcher:
|
|
702
|
+
st.error("Dispatcher not initialized. Please check logs.")
|
|
703
|
+
return
|
|
704
|
+
|
|
705
|
+
with st.spinner("🔄 Dispatching task to multi-agent system..."):
|
|
706
|
+
try:
|
|
707
|
+
start_time = time.time()
|
|
708
|
+
|
|
709
|
+
result = dispatcher.dispatch(
|
|
710
|
+
task_description=task_description.strip(),
|
|
711
|
+
roles=selected_roles if selected_roles else None,
|
|
712
|
+
mode=execution_mode,
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
duration = time.time() - start_time
|
|
716
|
+
|
|
717
|
+
st.session_state['last_dispatch_result'] = result
|
|
718
|
+
st.session_state['dispatch_duration'] = duration
|
|
719
|
+
|
|
720
|
+
st.success(f"✅ Task completed in {duration:.2f}s")
|
|
721
|
+
st.rerun()
|
|
722
|
+
|
|
723
|
+
except Exception as e:
|
|
724
|
+
st.error(f"❌ Dispatch failed: {e}")
|
|
725
|
+
st.exception(e)
|
|
726
|
+
|
|
727
|
+
with col_clear:
|
|
728
|
+
if st.button("🗑️ Clear Results", use_container_width=True):
|
|
729
|
+
if 'last_dispatch_result' in st.session_state:
|
|
730
|
+
del st.session_state['last_dispatch_result']
|
|
731
|
+
if 'dispatch_duration' in st.session_state:
|
|
732
|
+
del st.session_state['dispatch_duration']
|
|
733
|
+
st.rerun()
|
|
734
|
+
|
|
735
|
+
st.markdown("---")
|
|
736
|
+
|
|
737
|
+
if 'last_dispatch_result' in st.session_state:
|
|
738
|
+
result = st.session_state['last_dispatch_result']
|
|
739
|
+
duration = st.session_state.get('dispatch_duration', 0)
|
|
740
|
+
|
|
741
|
+
render_dispatch_result(result, duration)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def render_dispatch_result(result, duration: float):
|
|
745
|
+
"""Render dispatch result with metadata and formatted report."""
|
|
746
|
+
st.subheader("📊 Dispatch Result")
|
|
747
|
+
|
|
748
|
+
col_meta, col_timing = st.columns([2, 1])
|
|
749
|
+
|
|
750
|
+
with col_meta:
|
|
751
|
+
st.markdown("**Metadata**")
|
|
752
|
+
|
|
753
|
+
intent_match = result.intent_match
|
|
754
|
+
if intent_match:
|
|
755
|
+
st.markdown(f"- **Intent Type**: `{intent_match.get('intent_type', 'N/A')}`")
|
|
756
|
+
workflow_chain = intent_match.get('workflow_chain', [])
|
|
757
|
+
if workflow_chain:
|
|
758
|
+
st.markdown(f"- **Workflow**: `{' → '.join(workflow_chain[:5])}`")
|
|
759
|
+
st.markdown(f"- **Confidence**: `{intent_match.get('confidence', 0):.1%}`")
|
|
760
|
+
|
|
761
|
+
matched_roles = result.matched_roles
|
|
762
|
+
if matched_roles:
|
|
763
|
+
roles_display = []
|
|
764
|
+
for role_id in matched_roles:
|
|
765
|
+
icon = DashboardConfig.ROLE_ICONS.get(role_id, '🤖')
|
|
766
|
+
name = DashboardConfig.ROLE_NAMES.get(role_id, role_id)
|
|
767
|
+
roles_display.append(f"{icon} {name}")
|
|
768
|
+
st.markdown(f"- **Matched Roles**: {', '.join(roles_display)}")
|
|
769
|
+
|
|
770
|
+
st.markdown(f"- **Status**: {'✅ Success' if result.success else '❌ Failed'}")
|
|
771
|
+
st.markdown(f"- **Duration**: `{duration:.2f}s`")
|
|
772
|
+
|
|
773
|
+
with col_timing:
|
|
774
|
+
st.markdown("**Timing Breakdown**")
|
|
775
|
+
timing = result.details.get('timing', {})
|
|
776
|
+
if timing:
|
|
777
|
+
timing_data = [{"Step": k, "Time (s)": f"{v:.3f}"} for k, v in sorted(timing.items(), key=lambda x: x[1], reverse=True)]
|
|
778
|
+
st.dataframe(timing_data, use_container_width=True, hide_index=True)
|
|
779
|
+
|
|
780
|
+
st.markdown("---")
|
|
781
|
+
|
|
782
|
+
tabs_report, tabs_raw = st.tabs(["📝 Formatted Report", "🔍 Raw Data"])
|
|
783
|
+
|
|
784
|
+
with tabs_report:
|
|
785
|
+
st.markdown(result.summary or result.to_markdown())
|
|
786
|
+
|
|
787
|
+
with tabs_raw:
|
|
788
|
+
st.json(result.to_dict())
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def render_admin_users_page(auth: AuthManager):
|
|
792
|
+
"""Render Admin User Management page."""
|
|
793
|
+
st.header("👥 User Management")
|
|
794
|
+
st.markdown("---")
|
|
795
|
+
|
|
796
|
+
credentials = auth.credentials
|
|
797
|
+
|
|
798
|
+
if not credentials:
|
|
799
|
+
st.info("No users configured in deployment.yaml")
|
|
800
|
+
return
|
|
801
|
+
|
|
802
|
+
st.markdown(f"**Total Users:** {len(credentials)}")
|
|
803
|
+
|
|
804
|
+
for username, cred in credentials.items():
|
|
805
|
+
with st.expander(f"👤 {username}", expanded=False):
|
|
806
|
+
col1, col2, col3 = st.columns(3)
|
|
807
|
+
|
|
808
|
+
with col1:
|
|
809
|
+
st.markdown(f"**Name:** {cred.get('name', username)}")
|
|
810
|
+
st.markdown(f"**Email:** {cred.get('email', 'N/A')}")
|
|
811
|
+
|
|
812
|
+
with col2:
|
|
813
|
+
role = cred.get('role', 'viewer')
|
|
814
|
+
role_badge = {
|
|
815
|
+
"admin": "status-danger",
|
|
816
|
+
"operator": "status-warning",
|
|
817
|
+
"viewer": "status-secondary"
|
|
818
|
+
}.get(role, "status-secondary")
|
|
819
|
+
st.markdown(
|
|
820
|
+
f'<span class="status-badge {role_badge}">{role.upper()}</span>',
|
|
821
|
+
unsafe_allow_html=True
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
with col3:
|
|
825
|
+
new_role = st.selectbox(
|
|
826
|
+
f"Change Role",
|
|
827
|
+
options=["admin", "operator", "viewer"],
|
|
828
|
+
index=["admin", "operator", "viewer"].index(role) if role in ["admin", "operator", "viewer"] else 2,
|
|
829
|
+
key=f"role_{username}",
|
|
830
|
+
disabled=True,
|
|
831
|
+
help="Role changes require editing config/deployment.yaml"
|
|
832
|
+
)
|
|
833
|
+
st.caption("ℹ️ Edit config file to change roles")
|
|
834
|
+
|
|
835
|
+
st.markdown("---")
|
|
836
|
+
st.info("""
|
|
837
|
+
**User Management Notes:**
|
|
838
|
+
- Users are configured in `config/deployment.yaml`
|
|
839
|
+
- To add/edit users, modify the `authentication.credentials.usernames` section
|
|
840
|
+
- Restart the dashboard after configuration changes
|
|
841
|
+
""")
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def render_admin_system_config_page():
|
|
845
|
+
"""Render Admin System Configuration page (read-only)."""
|
|
846
|
+
st.header("⚙️ System Configuration")
|
|
847
|
+
st.markdown("---")
|
|
848
|
+
|
|
849
|
+
st.markdown("**Environment Variables**")
|
|
850
|
+
|
|
851
|
+
env_vars = {
|
|
852
|
+
"PYTHONPATH": os.environ.get("PYTHONPATH", "Not set"),
|
|
853
|
+
"HOME": os.environ.get("HOME", "Not set"),
|
|
854
|
+
"LANG": os.environ.get("LANG", "Not set"),
|
|
855
|
+
"TERM": os.environ.get("TERM", "Not set"),
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
env_data = [{"Variable": k, "Value": v} for k, v in env_vars.items()]
|
|
859
|
+
st.dataframe(env_data, use_container_width=True, hide_index=True)
|
|
860
|
+
|
|
861
|
+
st.markdown("---")
|
|
862
|
+
|
|
863
|
+
st.markdown("**System Information**")
|
|
864
|
+
|
|
865
|
+
sys_info = [
|
|
866
|
+
{"Item": "Python Version", "Value": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"},
|
|
867
|
+
{"Item": "Platform", "Value": sys.platform},
|
|
868
|
+
{"Item": "Working Directory", "Value": os.getcwd()},
|
|
869
|
+
{"Item": "DevSquad Version", "Value": "V3.6.0"},
|
|
870
|
+
{"Item": "Dashboard Mode", "Value": "Production-Grade"},
|
|
871
|
+
]
|
|
872
|
+
st.dataframe(sys_info, use_container_width=True, hide_index=True)
|
|
873
|
+
|
|
874
|
+
st.markdown("---")
|
|
875
|
+
|
|
876
|
+
st.markdown("**Configuration Status**")
|
|
877
|
+
|
|
878
|
+
config_items = [
|
|
879
|
+
{"Component": "Authentication", "Status": "✅ Enabled" if auth.credentials else "⚠️ No Config"},
|
|
880
|
+
{"Component": "Lifecycle Protocol", "Status": "✅ Loaded" if load_lifecycle_protocol() else "❌ Failed"},
|
|
881
|
+
{"Component": "Multi-Agent Dispatcher", "Status": "✅ Ready" if get_dispatcher() else "❌ Not Initialized"},
|
|
882
|
+
{"Component": "Auto-Refresh", "Status": f"{'✅ Active' if st.session_state.get('auto_refresh', False) else '⚪ Inactive'}"},
|
|
883
|
+
]
|
|
884
|
+
st.dataframe(config_items, use_container_width=True, hide_index=True)
|
|
885
|
+
|
|
886
|
+
st.markdown("---")
|
|
887
|
+
st.caption("ℹ️ This page is read-only. Configuration changes require editing source files.")
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def render_action_panel(protocol_data):
|
|
891
|
+
"""Render action control panel."""
|
|
892
|
+
st.subheader("🎮 Control Panel")
|
|
893
|
+
|
|
894
|
+
col1, col2, col3 = st.columns(3)
|
|
895
|
+
|
|
896
|
+
with col1:
|
|
897
|
+
if st.button("▶️ Run All Phases", type="primary", use_container_width=True):
|
|
898
|
+
st.session_state['action'] = 'run_all'
|
|
899
|
+
st.success("Initiated full lifecycle run...")
|
|
900
|
+
st.rerun()
|
|
901
|
+
|
|
902
|
+
with col2:
|
|
903
|
+
if st.button("🔄 Reset State", use_container_width=True):
|
|
904
|
+
st.session_state['action'] = 'reset'
|
|
905
|
+
st.info("Resetting lifecycle state...")
|
|
906
|
+
st.rerun()
|
|
907
|
+
|
|
908
|
+
with col3:
|
|
909
|
+
if st.button("📊 Generate Report", use_container_width=True):
|
|
910
|
+
st.session_state['action'] = 'report'
|
|
911
|
+
st.success("Generating benchmark report...")
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def handle_auto_refresh():
|
|
915
|
+
"""Handle auto-refresh logic with countdown timer."""
|
|
916
|
+
auto_refresh = st.session_state.get('auto_refresh', False)
|
|
917
|
+
|
|
918
|
+
if auto_refresh:
|
|
919
|
+
if 'refresh_start_time' not in st.session_state:
|
|
920
|
+
st.session_state.refresh_start_time = time.time()
|
|
921
|
+
|
|
922
|
+
elapsed = time.time() - st.session_state.refresh_start_time
|
|
923
|
+
remaining = max(0, 30 - int(elapsed))
|
|
924
|
+
|
|
925
|
+
st.session_state['refresh_countdown'] = remaining
|
|
926
|
+
|
|
927
|
+
if remaining <= 0:
|
|
928
|
+
st.session_state.refresh_start_time = time.time()
|
|
929
|
+
st.rerun()
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def render_sidebar(auth: AuthManager, current_user: Optional[User] = None):
|
|
933
|
+
"""Render sidebar navigation and controls with new pages."""
|
|
934
|
+
with st.sidebar:
|
|
935
|
+
st.header("Navigation")
|
|
936
|
+
|
|
937
|
+
page_options = [
|
|
938
|
+
"Overview",
|
|
939
|
+
"Phases",
|
|
940
|
+
"Mapping",
|
|
941
|
+
"Gates",
|
|
942
|
+
"Performance",
|
|
943
|
+
"Task Dispatch",
|
|
944
|
+
]
|
|
945
|
+
|
|
946
|
+
page_captions = [
|
|
947
|
+
"System overview & metrics",
|
|
948
|
+
"Phase timeline & details",
|
|
949
|
+
"CLI command mapping",
|
|
950
|
+
"Gate status monitor",
|
|
951
|
+
"Performance metrics",
|
|
952
|
+
"Create & manage tasks",
|
|
953
|
+
]
|
|
954
|
+
|
|
955
|
+
page = st.radio(
|
|
956
|
+
"Go to",
|
|
957
|
+
page_options,
|
|
958
|
+
captions=page_captions,
|
|
959
|
+
label_visibility="collapsed"
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
st.divider()
|
|
963
|
+
|
|
964
|
+
if current_user:
|
|
965
|
+
auth.get_login_button()
|
|
966
|
+
|
|
967
|
+
st.header("Settings")
|
|
968
|
+
|
|
969
|
+
auto_refresh = st.checkbox("⏱️ Auto Refresh (30s)", value=st.session_state.get('auto_refresh', False))
|
|
970
|
+
st.session_state['auto_refresh'] = auto_refresh
|
|
971
|
+
|
|
972
|
+
show_details = st.checkbox("📋 Show Details", value=True)
|
|
973
|
+
|
|
974
|
+
st.divider()
|
|
975
|
+
|
|
976
|
+
st.header("Quick Actions")
|
|
977
|
+
|
|
978
|
+
if st.button("🔄 Refresh Data", use_container_width=True):
|
|
979
|
+
st.rerun()
|
|
980
|
+
|
|
981
|
+
if st.button("📥 Export Status", use_container_width=True):
|
|
982
|
+
st.success("Status exported!")
|
|
983
|
+
|
|
984
|
+
if current_user and current_user.role.value == "admin":
|
|
985
|
+
st.divider()
|
|
986
|
+
st.header("⚙️ Admin")
|
|
987
|
+
|
|
988
|
+
if st.button("👥 Manage Users", use_container_width=True):
|
|
989
|
+
st.session_state.page = "admin_users"
|
|
990
|
+
|
|
991
|
+
if st.button("⚙️ System Config", use_container_width=True):
|
|
992
|
+
st.session_state.page = "system_config"
|
|
993
|
+
|
|
994
|
+
return page
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def render_footer(current_user: Optional[User] = None):
|
|
998
|
+
"""Render dashboard footer with version and session info."""
|
|
999
|
+
st.markdown("---")
|
|
1000
|
+
|
|
1001
|
+
col1, col2, col3 = st.columns([2, 1, 1])
|
|
1002
|
+
|
|
1003
|
+
with col1:
|
|
1004
|
+
st.markdown(
|
|
1005
|
+
"<center>**DevSquad V3.6.0** | Plan C | Production Ready | 🎨 Enhanced UI</center>",
|
|
1006
|
+
unsafe_allow_html=True
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
with col2:
|
|
1010
|
+
if current_user:
|
|
1011
|
+
st.caption(f"Session: {current_user.session_id[:8]}...")
|
|
1012
|
+
|
|
1013
|
+
with col3:
|
|
1014
|
+
st.caption(f"© {datetime.now().year} DevSquad Team")
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def main():
|
|
1018
|
+
"""Main dashboard entry point."""
|
|
1019
|
+
set_page_config()
|
|
1020
|
+
apply_custom_css()
|
|
1021
|
+
|
|
1022
|
+
auth = AuthManager(config_path=os.path.join(
|
|
1023
|
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
1024
|
+
"config",
|
|
1025
|
+
"deployment.yaml"
|
|
1026
|
+
))
|
|
1027
|
+
|
|
1028
|
+
auth.authenticate_streamlit()
|
|
1029
|
+
|
|
1030
|
+
current_user = auth.get_current_user()
|
|
1031
|
+
|
|
1032
|
+
page = render_sidebar(auth, current_user)
|
|
1033
|
+
|
|
1034
|
+
handle_auto_refresh()
|
|
1035
|
+
|
|
1036
|
+
render_header(current_user)
|
|
1037
|
+
|
|
1038
|
+
protocol_data = load_lifecycle_protocol()
|
|
1039
|
+
dispatcher = get_dispatcher()
|
|
1040
|
+
|
|
1041
|
+
can_execute = current_user.can_execute_phases() if current_user else False
|
|
1042
|
+
|
|
1043
|
+
if page == "Overview":
|
|
1044
|
+
render_metrics_overview(protocol_data)
|
|
1045
|
+
st.divider()
|
|
1046
|
+
if can_execute:
|
|
1047
|
+
render_action_panel(protocol_data)
|
|
1048
|
+
else:
|
|
1049
|
+
st.info("🔒 Phase execution requires Operator or Admin role")
|
|
1050
|
+
|
|
1051
|
+
elif page == "Phases":
|
|
1052
|
+
render_phase_timeline(protocol_data)
|
|
1053
|
+
|
|
1054
|
+
elif page == "Mapping":
|
|
1055
|
+
render_cli_mapping_table(protocol_data)
|
|
1056
|
+
|
|
1057
|
+
elif page == "Gates":
|
|
1058
|
+
render_gate_status_panel(protocol_data)
|
|
1059
|
+
|
|
1060
|
+
elif page == "Performance":
|
|
1061
|
+
render_performance_panel()
|
|
1062
|
+
|
|
1063
|
+
elif page == "Task Dispatch":
|
|
1064
|
+
if can_execute:
|
|
1065
|
+
render_task_dispatch_page(dispatcher)
|
|
1066
|
+
else:
|
|
1067
|
+
st.error("🔒 Task dispatch requires Operator or Admin role")
|
|
1068
|
+
st.info("Please contact your administrator to request elevated permissions.")
|
|
1069
|
+
|
|
1070
|
+
if st.session_state.get('page') == 'admin_users':
|
|
1071
|
+
if current_user and current_user.role.value == "admin":
|
|
1072
|
+
render_admin_users_page(auth)
|
|
1073
|
+
else:
|
|
1074
|
+
st.error("🔒 Access denied. Admin role required.")
|
|
1075
|
+
|
|
1076
|
+
if st.session_state.get('page') == 'system_config':
|
|
1077
|
+
if current_user and current_user.role.value == "admin":
|
|
1078
|
+
render_admin_system_config_page()
|
|
1079
|
+
else:
|
|
1080
|
+
st.error("🔒 Access denied. Admin role required.")
|
|
1081
|
+
|
|
1082
|
+
st.divider()
|
|
1083
|
+
|
|
1084
|
+
render_footer(current_user)
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
if __name__ == "__main__":
|
|
1088
|
+
main()
|