runbooks 0.7.9__py3-none-any.whl → 0.9.1__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.
- runbooks/__init__.py +1 -1
- runbooks/cfat/README.md +12 -1
- runbooks/cfat/__init__.py +1 -1
- runbooks/cfat/assessment/compliance.py +4 -1
- runbooks/cfat/assessment/runner.py +42 -34
- runbooks/cfat/models.py +1 -1
- runbooks/cloudops/__init__.py +123 -0
- runbooks/cloudops/base.py +385 -0
- runbooks/cloudops/cost_optimizer.py +811 -0
- runbooks/cloudops/infrastructure_optimizer.py +29 -0
- runbooks/cloudops/interfaces.py +828 -0
- runbooks/cloudops/lifecycle_manager.py +29 -0
- runbooks/cloudops/mcp_cost_validation.py +678 -0
- runbooks/cloudops/models.py +251 -0
- runbooks/cloudops/monitoring_automation.py +29 -0
- runbooks/cloudops/notebook_framework.py +676 -0
- runbooks/cloudops/security_enforcer.py +449 -0
- runbooks/common/__init__.py +152 -0
- runbooks/common/accuracy_validator.py +1039 -0
- runbooks/common/context_logger.py +440 -0
- runbooks/common/cross_module_integration.py +594 -0
- runbooks/common/enhanced_exception_handler.py +1108 -0
- runbooks/common/enterprise_audit_integration.py +634 -0
- runbooks/common/mcp_cost_explorer_integration.py +900 -0
- runbooks/common/mcp_integration.py +548 -0
- runbooks/common/performance_monitor.py +387 -0
- runbooks/common/profile_utils.py +216 -0
- runbooks/common/rich_utils.py +172 -1
- runbooks/feedback/user_feedback_collector.py +440 -0
- runbooks/finops/README.md +377 -458
- runbooks/finops/__init__.py +4 -21
- runbooks/finops/account_resolver.py +279 -0
- runbooks/finops/accuracy_cross_validator.py +638 -0
- runbooks/finops/aws_client.py +721 -36
- runbooks/finops/budget_integration.py +313 -0
- runbooks/finops/cli.py +59 -5
- runbooks/finops/cost_optimizer.py +1340 -0
- runbooks/finops/cost_processor.py +211 -37
- runbooks/finops/dashboard_router.py +900 -0
- runbooks/finops/dashboard_runner.py +990 -232
- runbooks/finops/embedded_mcp_validator.py +288 -0
- runbooks/finops/enhanced_dashboard_runner.py +8 -7
- runbooks/finops/enhanced_progress.py +327 -0
- runbooks/finops/enhanced_trend_visualization.py +423 -0
- runbooks/finops/finops_dashboard.py +184 -1829
- runbooks/finops/helpers.py +509 -196
- runbooks/finops/iam_guidance.py +400 -0
- runbooks/finops/markdown_exporter.py +466 -0
- runbooks/finops/multi_dashboard.py +1502 -0
- runbooks/finops/optimizer.py +15 -15
- runbooks/finops/profile_processor.py +2 -2
- runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/finops/runbooks.security.report_generator.log +0 -0
- runbooks/finops/runbooks.security.run_script.log +0 -0
- runbooks/finops/runbooks.security.security_export.log +0 -0
- runbooks/finops/schemas.py +589 -0
- runbooks/finops/service_mapping.py +195 -0
- runbooks/finops/single_dashboard.py +710 -0
- runbooks/finops/tests/test_reference_images_validation.py +1 -1
- runbooks/inventory/README.md +12 -1
- runbooks/inventory/core/collector.py +157 -29
- runbooks/inventory/list_ec2_instances.py +9 -6
- runbooks/inventory/list_ssm_parameters.py +10 -10
- runbooks/inventory/organizations_discovery.py +210 -164
- runbooks/inventory/rich_inventory_display.py +74 -107
- runbooks/inventory/run_on_multi_accounts.py +13 -13
- runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/inventory/runbooks.security.security_export.log +0 -0
- runbooks/main.py +1371 -240
- runbooks/metrics/dora_metrics_engine.py +711 -17
- runbooks/monitoring/performance_monitor.py +433 -0
- runbooks/operate/README.md +394 -0
- runbooks/operate/base.py +215 -47
- runbooks/operate/ec2_operations.py +435 -5
- runbooks/operate/iam_operations.py +598 -3
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/rds_operations.py +508 -0
- runbooks/operate/s3_operations.py +508 -0
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/remediation/README.md +489 -13
- runbooks/remediation/base.py +5 -3
- runbooks/remediation/commons.py +8 -4
- runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +506 -0
- runbooks/security/README.md +12 -1
- runbooks/security/__init__.py +265 -33
- runbooks/security/cloudops_automation_security_validator.py +1164 -0
- runbooks/security/compliance_automation.py +12 -10
- runbooks/security/compliance_automation_engine.py +1021 -0
- runbooks/security/enterprise_security_framework.py +930 -0
- runbooks/security/enterprise_security_policies.json +293 -0
- runbooks/security/executive_security_dashboard.py +1247 -0
- runbooks/security/integration_test_enterprise_security.py +879 -0
- runbooks/security/module_security_integrator.py +641 -0
- runbooks/security/multi_account_security_controls.py +2254 -0
- runbooks/security/real_time_security_monitor.py +1196 -0
- runbooks/security/report_generator.py +1 -1
- runbooks/security/run_script.py +4 -8
- runbooks/security/security_baseline_tester.py +39 -52
- runbooks/security/security_export.py +99 -120
- runbooks/sre/README.md +472 -0
- runbooks/sre/__init__.py +33 -0
- runbooks/sre/mcp_reliability_engine.py +1049 -0
- runbooks/sre/performance_optimization_engine.py +1032 -0
- runbooks/sre/production_monitoring_framework.py +584 -0
- runbooks/sre/reliability_monitoring_framework.py +1011 -0
- runbooks/validation/__init__.py +2 -2
- runbooks/validation/benchmark.py +154 -149
- runbooks/validation/cli.py +159 -147
- runbooks/validation/mcp_validator.py +291 -248
- runbooks/vpc/README.md +478 -0
- runbooks/vpc/__init__.py +2 -2
- runbooks/vpc/manager_interface.py +366 -351
- runbooks/vpc/networking_wrapper.py +68 -36
- runbooks/vpc/rich_formatters.py +22 -8
- runbooks-0.9.1.dist-info/METADATA +308 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/RECORD +120 -59
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/entry_points.txt +1 -1
- runbooks/finops/cross_validation.py +0 -375
- runbooks-0.7.9.dist-info/METADATA +0 -636
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/WHEEL +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/top_level.txt +0 -0
@@ -8,31 +8,33 @@ optimization dashboard.
|
|
8
8
|
|
9
9
|
import json
|
10
10
|
import logging
|
11
|
+
from dataclasses import asdict, dataclass
|
11
12
|
from datetime import datetime, timedelta
|
13
|
+
from enum import Enum
|
12
14
|
from pathlib import Path
|
13
15
|
from typing import Any, Dict, List, Optional, Tuple
|
14
|
-
from dataclasses import dataclass, asdict
|
15
|
-
from enum import Enum
|
16
16
|
|
17
17
|
import pandas as pd
|
18
18
|
from rich.console import Console
|
19
|
-
from rich.table import Table
|
20
19
|
from rich.panel import Panel
|
21
20
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
21
|
+
from rich.table import Table
|
22
22
|
|
23
23
|
logger = logging.getLogger(__name__)
|
24
24
|
|
25
25
|
|
26
26
|
class BusinessPriority(Enum):
|
27
27
|
"""Business priority levels for manager decision making"""
|
28
|
+
|
28
29
|
CRITICAL = "Critical"
|
29
|
-
HIGH = "High"
|
30
|
+
HIGH = "High"
|
30
31
|
MEDIUM = "Medium"
|
31
32
|
LOW = "Low"
|
32
33
|
|
33
34
|
|
34
35
|
class RiskLevel(Enum):
|
35
36
|
"""Risk assessment levels for business decisions"""
|
37
|
+
|
36
38
|
MINIMAL = "Minimal"
|
37
39
|
LOW = "Low"
|
38
40
|
MEDIUM = "Medium"
|
@@ -42,6 +44,7 @@ class RiskLevel(Enum):
|
|
42
44
|
@dataclass
|
43
45
|
class BusinessRecommendation:
|
44
46
|
"""Business-focused recommendation structure"""
|
47
|
+
|
45
48
|
title: str
|
46
49
|
executive_summary: str
|
47
50
|
monthly_savings: float
|
@@ -59,6 +62,7 @@ class BusinessRecommendation:
|
|
59
62
|
@dataclass
|
60
63
|
class ManagerDashboardConfig:
|
61
64
|
"""Configuration for manager dashboard behavior"""
|
65
|
+
|
62
66
|
safety_mode: bool = True
|
63
67
|
auto_export: bool = True
|
64
68
|
executive_summaries_only: bool = False
|
@@ -66,37 +70,34 @@ class ManagerDashboardConfig:
|
|
66
70
|
target_savings_percentage: float = 30.0
|
67
71
|
max_implementation_weeks: int = 12
|
68
72
|
preferred_export_formats: List[str] = None
|
69
|
-
|
73
|
+
|
70
74
|
def __post_init__(self):
|
71
75
|
if self.preferred_export_formats is None:
|
72
|
-
self.preferred_export_formats = [
|
76
|
+
self.preferred_export_formats = ["json", "csv", "excel"]
|
73
77
|
|
74
78
|
|
75
79
|
class VPCManagerInterface:
|
76
80
|
"""
|
77
81
|
Manager-friendly interface for VPC cost optimization
|
78
|
-
|
82
|
+
|
79
83
|
This class provides high-level business methods that abstract away
|
80
84
|
technical complexity while providing comprehensive cost optimization
|
81
85
|
insights for executive decision making.
|
82
86
|
"""
|
83
|
-
|
87
|
+
|
84
88
|
def __init__(self, console: Optional[Console] = None):
|
85
89
|
self.console = console or Console(force_jupyter=True, width=100)
|
86
90
|
self.config = ManagerDashboardConfig()
|
87
91
|
self.analysis_results = {}
|
88
92
|
self.business_recommendations = []
|
89
93
|
self.export_directory = Path("./exports/manager_dashboard")
|
90
|
-
|
94
|
+
|
91
95
|
def configure_for_business_user(
|
92
|
-
self,
|
93
|
-
safety_mode: bool = True,
|
94
|
-
target_savings: float = 30.0,
|
95
|
-
approval_threshold: float = 1000.0
|
96
|
+
self, safety_mode: bool = True, target_savings: float = 30.0, approval_threshold: float = 1000.0
|
96
97
|
) -> None:
|
97
98
|
"""
|
98
99
|
Configure interface for business user preferences
|
99
|
-
|
100
|
+
|
100
101
|
Args:
|
101
102
|
safety_mode: Enable dry-run only mode for safety
|
102
103
|
target_savings: Target cost reduction percentage
|
@@ -105,484 +106,501 @@ class VPCManagerInterface:
|
|
105
106
|
self.config.safety_mode = safety_mode
|
106
107
|
self.config.target_savings_percentage = target_savings
|
107
108
|
self.config.approval_threshold = approval_threshold
|
108
|
-
|
109
|
-
self.console.print(
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
) -> Dict[str, Any]:
|
109
|
+
|
110
|
+
self.console.print(
|
111
|
+
Panel.fit(
|
112
|
+
f"[bold green]✅ Manager Interface Configured[/bold green]\n\n"
|
113
|
+
f"Safety Mode: {'[green]ON[/green]' if safety_mode else '[red]OFF[/red]'}\n"
|
114
|
+
f"Target Savings: [blue]{target_savings}%[/blue]\n"
|
115
|
+
f"Approval Threshold: [yellow]${approval_threshold:,.2f}[/yellow]",
|
116
|
+
title="Business Configuration",
|
117
|
+
)
|
118
|
+
)
|
119
|
+
|
120
|
+
def analyze_cost_optimization_opportunity(self, vpc_analysis_results: Dict[str, Any]) -> Dict[str, Any]:
|
121
121
|
"""
|
122
122
|
Convert technical VPC analysis into business-focused insights
|
123
|
-
|
123
|
+
|
124
124
|
Args:
|
125
125
|
vpc_analysis_results: Raw technical analysis from VPC wrapper
|
126
|
-
|
126
|
+
|
127
127
|
Returns:
|
128
128
|
Dictionary with business-focused analysis results
|
129
129
|
"""
|
130
130
|
self.console.print("[cyan]🔍 Converting technical analysis to business insights...[/cyan]")
|
131
|
-
|
131
|
+
|
132
132
|
# Extract business metrics
|
133
133
|
business_analysis = {
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
134
|
+
"executive_summary": self._create_executive_summary(vpc_analysis_results),
|
135
|
+
"financial_impact": self._calculate_financial_impact(vpc_analysis_results),
|
136
|
+
"risk_assessment": self._assess_business_risks(vpc_analysis_results),
|
137
|
+
"implementation_plan": self._create_implementation_plan(vpc_analysis_results),
|
138
|
+
"success_metrics": self._define_success_metrics(vpc_analysis_results),
|
139
|
+
"approval_requirements": self._determine_approval_requirements(vpc_analysis_results),
|
140
140
|
}
|
141
|
-
|
141
|
+
|
142
142
|
# Store for later export
|
143
143
|
self.analysis_results = business_analysis
|
144
|
-
|
144
|
+
|
145
145
|
# Generate business recommendations
|
146
|
-
self.business_recommendations = self._generate_business_recommendations(
|
147
|
-
|
148
|
-
)
|
149
|
-
|
146
|
+
self.business_recommendations = self._generate_business_recommendations(vpc_analysis_results, business_analysis)
|
147
|
+
|
150
148
|
return business_analysis
|
151
|
-
|
149
|
+
|
152
150
|
def _create_executive_summary(self, analysis: Dict[str, Any]) -> Dict[str, Any]:
|
153
151
|
"""Create executive summary from technical analysis"""
|
154
|
-
|
152
|
+
|
155
153
|
# Extract key financial metrics
|
156
154
|
total_cost = 0
|
157
155
|
total_savings = 0
|
158
|
-
|
159
|
-
for component in [
|
156
|
+
|
157
|
+
for component in ["nat_gateways", "vpc_endpoints", "transit_gateway"]:
|
160
158
|
if component in analysis:
|
161
159
|
comp_data = analysis[component]
|
162
|
-
total_cost += comp_data.get(
|
163
|
-
total_savings += comp_data.get(
|
164
|
-
|
160
|
+
total_cost += comp_data.get("total_cost", 0) or comp_data.get("total_monthly_cost", 0)
|
161
|
+
total_savings += comp_data.get("optimization_potential", 0) or comp_data.get("potential_savings", 0)
|
162
|
+
|
165
163
|
savings_percentage = (total_savings / total_cost * 100) if total_cost > 0 else 0
|
166
164
|
target_achieved = savings_percentage >= self.config.target_savings_percentage
|
167
|
-
|
165
|
+
|
168
166
|
return {
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
167
|
+
"current_monthly_cost": total_cost,
|
168
|
+
"potential_monthly_savings": total_savings,
|
169
|
+
"savings_percentage": savings_percentage,
|
170
|
+
"annual_savings_potential": total_savings * 12,
|
171
|
+
"target_achieved": target_achieved,
|
172
|
+
"payback_period_months": 1, # Most VPC optimizations have immediate payback
|
173
|
+
"confidence_level": "High",
|
174
|
+
"business_case_strength": "Excellent"
|
175
|
+
if savings_percentage >= 30
|
176
|
+
else "Good"
|
177
|
+
if savings_percentage >= 20
|
178
|
+
else "Moderate",
|
177
179
|
}
|
178
|
-
|
180
|
+
|
179
181
|
def _calculate_financial_impact(self, analysis: Dict[str, Any]) -> Dict[str, Any]:
|
180
182
|
"""Calculate comprehensive financial impact"""
|
181
|
-
|
183
|
+
|
182
184
|
exec_summary = self._create_executive_summary(analysis)
|
183
|
-
|
185
|
+
|
184
186
|
return {
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
187
|
+
"immediate_savings": {
|
188
|
+
"monthly": exec_summary["potential_monthly_savings"],
|
189
|
+
"quarterly": exec_summary["potential_monthly_savings"] * 3,
|
190
|
+
"annual": exec_summary["annual_savings_potential"],
|
191
|
+
},
|
192
|
+
"cost_avoidance": {
|
193
|
+
"description": "Prevents future cost growth from inefficient resources",
|
194
|
+
"annual_value": exec_summary["annual_savings_potential"] * 0.1, # 10% growth avoidance
|
189
195
|
},
|
190
|
-
|
191
|
-
|
192
|
-
|
196
|
+
"implementation_cost": {
|
197
|
+
"personnel_time": "Existing team allocation",
|
198
|
+
"infrastructure_changes": "Minimal - configuration only",
|
199
|
+
"estimated_hours": 40, # Conservative estimate
|
200
|
+
"estimated_cost": 5000, # Based on average engineer cost
|
193
201
|
},
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
202
|
+
"roi_analysis": {
|
203
|
+
"investment": 5000,
|
204
|
+
"annual_return": exec_summary["annual_savings_potential"],
|
205
|
+
"roi_percentage": (exec_summary["annual_savings_potential"] / 5000 * 100)
|
206
|
+
if exec_summary["annual_savings_potential"] > 0
|
207
|
+
else 0,
|
208
|
+
"payback_months": max(1, 5000 / max(exec_summary["potential_monthly_savings"], 1)),
|
199
209
|
},
|
200
|
-
'roi_analysis': {
|
201
|
-
'investment': 5000,
|
202
|
-
'annual_return': exec_summary['annual_savings_potential'],
|
203
|
-
'roi_percentage': (exec_summary['annual_savings_potential'] / 5000 * 100) if exec_summary['annual_savings_potential'] > 0 else 0,
|
204
|
-
'payback_months': max(1, 5000 / max(exec_summary['potential_monthly_savings'], 1))
|
205
|
-
}
|
206
210
|
}
|
207
|
-
|
211
|
+
|
208
212
|
def _assess_business_risks(self, analysis: Dict[str, Any]) -> Dict[str, Any]:
|
209
213
|
"""Assess business risks of optimization implementation"""
|
210
|
-
|
211
|
-
total_cost = sum(
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
214
|
+
|
215
|
+
total_cost = sum(
|
216
|
+
[
|
217
|
+
analysis.get("nat_gateways", {}).get("total_cost", 0),
|
218
|
+
analysis.get("vpc_endpoints", {}).get("total_cost", 0),
|
219
|
+
analysis.get("transit_gateway", {}).get("total_monthly_cost", 0),
|
220
|
+
]
|
221
|
+
)
|
222
|
+
|
217
223
|
# Risk assessment based on cost and complexity
|
218
224
|
risk_level = RiskLevel.LOW
|
219
225
|
if total_cost > 500:
|
220
226
|
risk_level = RiskLevel.MEDIUM
|
221
227
|
if total_cost > 1000:
|
222
228
|
risk_level = RiskLevel.HIGH
|
223
|
-
|
229
|
+
|
224
230
|
return {
|
225
|
-
|
226
|
-
|
231
|
+
"overall_risk": risk_level.value,
|
232
|
+
"risk_factors": [
|
227
233
|
{
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
234
|
+
"factor": "Service disruption during implementation",
|
235
|
+
"probability": "Low",
|
236
|
+
"impact": "Medium",
|
237
|
+
"mitigation": "Gradual rollout with testing",
|
232
238
|
},
|
233
239
|
{
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
240
|
+
"factor": "Configuration errors",
|
241
|
+
"probability": "Low",
|
242
|
+
"impact": "High",
|
243
|
+
"mitigation": "Dry-run validation and peer review",
|
238
244
|
},
|
239
245
|
{
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
}
|
246
|
+
"factor": "Cost increase if misconfigured",
|
247
|
+
"probability": "Very Low",
|
248
|
+
"impact": "Medium",
|
249
|
+
"mitigation": "Cost monitoring and automated alerts",
|
250
|
+
},
|
245
251
|
],
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
+
"mitigation_strategies": [
|
253
|
+
"Implement changes in non-production first",
|
254
|
+
"Use Infrastructure as Code for consistency",
|
255
|
+
"Enable comprehensive monitoring and alerting",
|
256
|
+
"Maintain rollback procedures",
|
257
|
+
"Conduct thorough testing before production",
|
252
258
|
],
|
253
|
-
|
259
|
+
"business_continuity": "No impact expected with proper implementation",
|
254
260
|
}
|
255
|
-
|
261
|
+
|
256
262
|
def _create_implementation_plan(self, analysis: Dict[str, Any]) -> Dict[str, Any]:
|
257
263
|
"""Create business-focused implementation plan"""
|
258
|
-
|
264
|
+
|
259
265
|
exec_summary = self._create_executive_summary(analysis)
|
260
|
-
|
266
|
+
|
261
267
|
phases = [
|
262
268
|
{
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
269
|
+
"phase": 1,
|
270
|
+
"name": "Quick Wins & Foundation",
|
271
|
+
"duration": "2-4 weeks",
|
272
|
+
"savings": exec_summary["potential_monthly_savings"] * 0.4, # 40% of savings
|
273
|
+
"activities": [
|
274
|
+
"Deploy free Gateway VPC Endpoints",
|
275
|
+
"Implement cost monitoring",
|
276
|
+
"Analyze underutilized resources",
|
271
277
|
],
|
272
|
-
|
273
|
-
|
278
|
+
"risk": "Low",
|
279
|
+
"approval_required": exec_summary["potential_monthly_savings"] * 12 > self.config.approval_threshold,
|
274
280
|
},
|
275
281
|
{
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
282
|
+
"phase": 2,
|
283
|
+
"name": "Strategic Optimization",
|
284
|
+
"duration": "4-8 weeks",
|
285
|
+
"savings": exec_summary["potential_monthly_savings"] * 0.6, # 60% of savings
|
286
|
+
"activities": [
|
287
|
+
"Optimize NAT Gateway usage",
|
288
|
+
"Deploy strategic Interface endpoints",
|
289
|
+
"Consolidate Transit Gateway attachments",
|
284
290
|
],
|
285
|
-
|
286
|
-
|
287
|
-
}
|
291
|
+
"risk": "Medium",
|
292
|
+
"approval_required": True,
|
293
|
+
},
|
288
294
|
]
|
289
|
-
|
295
|
+
|
290
296
|
return {
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
297
|
+
"total_timeline": "6-12 weeks",
|
298
|
+
"phases": phases,
|
299
|
+
"resource_requirements": {
|
300
|
+
"project_manager": "25% allocation for 3 months",
|
301
|
+
"cloud_engineer": "75% allocation for 6 weeks",
|
302
|
+
"solutions_architect": "50% allocation for 2 weeks",
|
303
|
+
"security_engineer": "25% allocation for 1 week",
|
298
304
|
},
|
299
|
-
|
300
|
-
f
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
]
|
305
|
+
"success_criteria": [
|
306
|
+
f"Achieve {self.config.target_savings_percentage}% cost reduction",
|
307
|
+
"No service disruptions during implementation",
|
308
|
+
"All changes validated in non-production first",
|
309
|
+
"Cost monitoring and alerting operational",
|
310
|
+
],
|
305
311
|
}
|
306
|
-
|
312
|
+
|
307
313
|
def _define_success_metrics(self, analysis: Dict[str, Any]) -> List[Dict[str, Any]]:
|
308
314
|
"""Define business success metrics"""
|
309
|
-
|
315
|
+
|
310
316
|
exec_summary = self._create_executive_summary(analysis)
|
311
|
-
|
317
|
+
|
312
318
|
return [
|
313
319
|
{
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
320
|
+
"metric": "Monthly Cost Reduction",
|
321
|
+
"target": f"${exec_summary['potential_monthly_savings']:.2f}",
|
322
|
+
"measurement": "AWS billing comparison",
|
323
|
+
"frequency": "Monthly",
|
318
324
|
},
|
319
325
|
{
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
326
|
+
"metric": "Savings Percentage",
|
327
|
+
"target": f"{self.config.target_savings_percentage}%+",
|
328
|
+
"measurement": "Automated cost analysis",
|
329
|
+
"frequency": "Weekly",
|
324
330
|
},
|
325
331
|
{
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
332
|
+
"metric": "Implementation Timeline",
|
333
|
+
"target": "< 12 weeks",
|
334
|
+
"measurement": "Project milestone tracking",
|
335
|
+
"frequency": "Weekly",
|
330
336
|
},
|
331
337
|
{
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
338
|
+
"metric": "Business Continuity",
|
339
|
+
"target": "Zero disruptions",
|
340
|
+
"measurement": "Incident tracking",
|
341
|
+
"frequency": "Continuous",
|
336
342
|
},
|
337
343
|
{
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
}
|
344
|
+
"metric": "ROI Achievement",
|
345
|
+
"target": f"> {((exec_summary['annual_savings_potential'] / 5000 * 100) if exec_summary['annual_savings_potential'] > 0 else 0):.0f}%",
|
346
|
+
"measurement": "Financial analysis",
|
347
|
+
"frequency": "Quarterly",
|
348
|
+
},
|
343
349
|
]
|
344
|
-
|
350
|
+
|
345
351
|
def _determine_approval_requirements(self, analysis: Dict[str, Any]) -> Dict[str, Any]:
|
346
352
|
"""Determine what approvals are needed"""
|
347
|
-
|
353
|
+
|
348
354
|
exec_summary = self._create_executive_summary(analysis)
|
349
|
-
annual_impact = exec_summary[
|
350
|
-
|
355
|
+
annual_impact = exec_summary["annual_savings_potential"]
|
356
|
+
|
351
357
|
approvals_needed = []
|
352
|
-
|
358
|
+
|
353
359
|
if annual_impact > self.config.approval_threshold:
|
354
|
-
approvals_needed.append(
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
360
|
+
approvals_needed.append(
|
361
|
+
{
|
362
|
+
"type": "Financial Approval",
|
363
|
+
"reason": f"Annual impact ${annual_impact:,.2f} exceeds threshold",
|
364
|
+
"approver": "Finance/Management",
|
365
|
+
"timeline": "1-2 weeks",
|
366
|
+
}
|
367
|
+
)
|
368
|
+
|
369
|
+
if exec_summary["current_monthly_cost"] > 500:
|
370
|
+
approvals_needed.append(
|
371
|
+
{
|
372
|
+
"type": "Technical Approval",
|
373
|
+
"reason": "High-value infrastructure changes",
|
374
|
+
"approver": "Solutions Architecture",
|
375
|
+
"timeline": "1 week",
|
376
|
+
}
|
377
|
+
)
|
378
|
+
|
369
379
|
return {
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
380
|
+
"approvals_required": len(approvals_needed) > 0,
|
381
|
+
"approval_list": approvals_needed,
|
382
|
+
"estimated_approval_timeline": "2-3 weeks" if approvals_needed else "0 weeks",
|
383
|
+
"bypass_criteria": "Emergency cost optimization directive",
|
374
384
|
}
|
375
|
-
|
385
|
+
|
376
386
|
def _generate_business_recommendations(
|
377
|
-
self,
|
378
|
-
analysis: Dict[str, Any],
|
379
|
-
business_analysis: Dict[str, Any]
|
387
|
+
self, analysis: Dict[str, Any], business_analysis: Dict[str, Any]
|
380
388
|
) -> List[BusinessRecommendation]:
|
381
389
|
"""Generate business-focused recommendations"""
|
382
|
-
|
390
|
+
|
383
391
|
recommendations = []
|
384
|
-
exec_summary = business_analysis[
|
385
|
-
|
392
|
+
exec_summary = business_analysis["executive_summary"]
|
393
|
+
|
386
394
|
# Quick Win: Gateway VPC Endpoints
|
387
|
-
recommendations.append(
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
risk_level=RiskLevel.MINIMAL,
|
395
|
-
resource_requirements=["Cloud Engineer (1 week)"],
|
396
|
-
success_metrics=["NAT Gateway data transfer reduction", "Zero implementation cost"],
|
397
|
-
approval_required=False,
|
398
|
-
quick_win=True,
|
399
|
-
strategic_value="Foundation for advanced VPC optimization"
|
400
|
-
))
|
401
|
-
|
402
|
-
# Strategic: NAT Gateway Optimization
|
403
|
-
if analysis.get('nat_gateways', {}).get('optimization_potential', 0) > 20:
|
404
|
-
recommendations.append(BusinessRecommendation(
|
405
|
-
title="Optimize Underutilized NAT Gateways",
|
406
|
-
executive_summary="Remove or consolidate NAT Gateways with low utilization to eliminate waste",
|
407
|
-
monthly_savings=analysis['nat_gateways']['optimization_potential'],
|
408
|
-
annual_impact=analysis['nat_gateways']['optimization_potential'] * 12,
|
409
|
-
implementation_timeline="3-4 weeks",
|
395
|
+
recommendations.append(
|
396
|
+
BusinessRecommendation(
|
397
|
+
title="Deploy Free Gateway VPC Endpoints",
|
398
|
+
executive_summary="Eliminate NAT Gateway charges for S3 and DynamoDB traffic with zero-cost solution",
|
399
|
+
monthly_savings=25.0,
|
400
|
+
annual_impact=300.0,
|
401
|
+
implementation_timeline="1-2 weeks",
|
410
402
|
business_priority=BusinessPriority.HIGH,
|
411
|
-
risk_level=RiskLevel.
|
412
|
-
resource_requirements=["Cloud Engineer (
|
413
|
-
success_metrics=["
|
414
|
-
approval_required=
|
415
|
-
quick_win=
|
416
|
-
strategic_value="
|
417
|
-
)
|
418
|
-
|
403
|
+
risk_level=RiskLevel.MINIMAL,
|
404
|
+
resource_requirements=["Cloud Engineer (1 week)"],
|
405
|
+
success_metrics=["NAT Gateway data transfer reduction", "Zero implementation cost"],
|
406
|
+
approval_required=False,
|
407
|
+
quick_win=True,
|
408
|
+
strategic_value="Foundation for advanced VPC optimization",
|
409
|
+
)
|
410
|
+
)
|
411
|
+
|
412
|
+
# Strategic: NAT Gateway Optimization
|
413
|
+
if analysis.get("nat_gateways", {}).get("optimization_potential", 0) > 20:
|
414
|
+
recommendations.append(
|
415
|
+
BusinessRecommendation(
|
416
|
+
title="Optimize Underutilized NAT Gateways",
|
417
|
+
executive_summary="Remove or consolidate NAT Gateways with low utilization to eliminate waste",
|
418
|
+
monthly_savings=analysis["nat_gateways"]["optimization_potential"],
|
419
|
+
annual_impact=analysis["nat_gateways"]["optimization_potential"] * 12,
|
420
|
+
implementation_timeline="3-4 weeks",
|
421
|
+
business_priority=BusinessPriority.HIGH,
|
422
|
+
risk_level=RiskLevel.MEDIUM,
|
423
|
+
resource_requirements=["Cloud Engineer (2 weeks)", "Security review"],
|
424
|
+
success_metrics=["Resource utilization improvement", "Cost per connection reduction"],
|
425
|
+
approval_required=analysis["nat_gateways"]["optimization_potential"] * 12
|
426
|
+
> self.config.approval_threshold,
|
427
|
+
quick_win=False,
|
428
|
+
strategic_value="Long-term infrastructure efficiency improvement",
|
429
|
+
)
|
430
|
+
)
|
431
|
+
|
419
432
|
# Advanced: Transit Gateway Optimization
|
420
|
-
tgw_savings = analysis.get(
|
433
|
+
tgw_savings = analysis.get("transit_gateway", {}).get("potential_savings", 0)
|
421
434
|
if tgw_savings > 30:
|
422
|
-
recommendations.append(
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
435
|
+
recommendations.append(
|
436
|
+
BusinessRecommendation(
|
437
|
+
title="Optimize Multi-Account Network Architecture",
|
438
|
+
executive_summary="Consolidate Transit Gateway attachments and implement centralized endpoints",
|
439
|
+
monthly_savings=tgw_savings,
|
440
|
+
annual_impact=tgw_savings * 12,
|
441
|
+
implementation_timeline="6-8 weeks",
|
442
|
+
business_priority=BusinessPriority.MEDIUM,
|
443
|
+
risk_level=RiskLevel.HIGH,
|
444
|
+
resource_requirements=["Solutions Architect (3 weeks)", "Network Engineer (2 weeks)"],
|
445
|
+
success_metrics=["Multi-account cost efficiency", "Network architecture optimization"],
|
446
|
+
approval_required=True,
|
447
|
+
quick_win=False,
|
448
|
+
strategic_value="Enterprise-scale network optimization and future-proofing",
|
449
|
+
)
|
450
|
+
)
|
451
|
+
|
437
452
|
return recommendations
|
438
|
-
|
453
|
+
|
439
454
|
def generate_executive_presentation(self) -> Dict[str, Any]:
|
440
455
|
"""Generate executive presentation content"""
|
441
|
-
|
456
|
+
|
442
457
|
if not self.analysis_results:
|
443
458
|
raise ValueError("No analysis results available. Run cost analysis first.")
|
444
|
-
|
445
|
-
exec_summary = self.analysis_results[
|
446
|
-
financial_impact = self.analysis_results[
|
447
|
-
|
459
|
+
|
460
|
+
exec_summary = self.analysis_results["executive_summary"]
|
461
|
+
financial_impact = self.analysis_results["financial_impact"]
|
462
|
+
|
448
463
|
presentation = {
|
449
|
-
|
450
|
-
|
451
|
-
|
464
|
+
"slide_1": {
|
465
|
+
"title": "VPC Cost Optimization Opportunity",
|
466
|
+
"content": [
|
452
467
|
f"Current monthly cost: ${exec_summary['current_monthly_cost']:,.2f}",
|
453
468
|
f"Potential savings: ${exec_summary['potential_monthly_savings']:,.2f} ({exec_summary['savings_percentage']:.1f}%)",
|
454
469
|
f"Annual impact: ${exec_summary['annual_savings_potential']:,.2f}",
|
455
|
-
f"Business case: {exec_summary['business_case_strength']}"
|
456
|
-
]
|
470
|
+
f"Business case: {exec_summary['business_case_strength']}",
|
471
|
+
],
|
457
472
|
},
|
458
|
-
|
459
|
-
|
460
|
-
|
473
|
+
"slide_2": {
|
474
|
+
"title": "Financial Impact & ROI",
|
475
|
+
"content": [
|
461
476
|
f"ROI: {financial_impact['roi_analysis']['roi_percentage']:.0f}%",
|
462
477
|
f"Payback period: {financial_impact['roi_analysis']['payback_months']:.0f} months",
|
463
478
|
f"Implementation cost: ${financial_impact['implementation_cost']['estimated_cost']:,.2f}",
|
464
|
-
f"Net annual benefit: ${exec_summary['annual_savings_potential'] - financial_impact['implementation_cost']['estimated_cost']:,.2f}"
|
465
|
-
]
|
479
|
+
f"Net annual benefit: ${exec_summary['annual_savings_potential'] - financial_impact['implementation_cost']['estimated_cost']:,.2f}",
|
480
|
+
],
|
466
481
|
},
|
467
|
-
|
468
|
-
|
469
|
-
|
482
|
+
"slide_3": {
|
483
|
+
"title": "Implementation Plan",
|
484
|
+
"content": [
|
470
485
|
"Phase 1: Quick wins (2-4 weeks)",
|
471
|
-
"Phase 2: Strategic optimization (4-8 weeks)",
|
486
|
+
"Phase 2: Strategic optimization (4-8 weeks)",
|
472
487
|
f"Total timeline: {self.analysis_results['implementation_plan']['total_timeline']}",
|
473
|
-
f"Risk level: {self.analysis_results['risk_assessment']['overall_risk']}"
|
474
|
-
]
|
488
|
+
f"Risk level: {self.analysis_results['risk_assessment']['overall_risk']}",
|
489
|
+
],
|
475
490
|
},
|
476
|
-
|
477
|
-
|
478
|
-
|
491
|
+
"slide_4": {
|
492
|
+
"title": "Business Benefits",
|
493
|
+
"content": [
|
479
494
|
"Immediate cost reduction",
|
480
495
|
"Improved network security through VPC endpoints",
|
481
496
|
"Enhanced operational efficiency",
|
482
|
-
"Foundation for ongoing optimization"
|
483
|
-
]
|
497
|
+
"Foundation for ongoing optimization",
|
498
|
+
],
|
484
499
|
},
|
485
|
-
|
486
|
-
|
487
|
-
|
500
|
+
"slide_5": {
|
501
|
+
"title": "Next Steps",
|
502
|
+
"content": [
|
488
503
|
"Approve Phase 1 implementation",
|
489
504
|
"Assign technical resources",
|
490
505
|
"Schedule weekly progress reviews",
|
491
|
-
"Plan Phase 2 strategic optimization"
|
492
|
-
]
|
493
|
-
}
|
506
|
+
"Plan Phase 2 strategic optimization",
|
507
|
+
],
|
508
|
+
},
|
494
509
|
}
|
495
|
-
|
510
|
+
|
496
511
|
return presentation
|
497
|
-
|
512
|
+
|
498
513
|
def export_manager_friendly_reports(self, timestamp: Optional[str] = None) -> Dict[str, str]:
|
499
514
|
"""Export comprehensive manager-friendly reports"""
|
500
|
-
|
515
|
+
|
501
516
|
if not self.analysis_results:
|
502
517
|
raise ValueError("No analysis results to export")
|
503
|
-
|
518
|
+
|
504
519
|
timestamp = timestamp or datetime.now().strftime("%Y%m%d_%H%M%S")
|
505
520
|
self.export_directory.mkdir(parents=True, exist_ok=True)
|
506
|
-
|
521
|
+
|
507
522
|
exported_files = {}
|
508
|
-
|
523
|
+
|
509
524
|
# 1. Executive Summary (JSON)
|
510
525
|
executive_report = {
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
526
|
+
"metadata": {
|
527
|
+
"report_type": "vpc_cost_optimization_executive",
|
528
|
+
"generated_at": datetime.now().isoformat(),
|
529
|
+
"version": "2.0",
|
515
530
|
},
|
516
|
-
|
517
|
-
|
518
|
-
|
531
|
+
"analysis_results": self.analysis_results,
|
532
|
+
"business_recommendations": [asdict(rec) for rec in self.business_recommendations],
|
533
|
+
"executive_presentation": self.generate_executive_presentation(),
|
519
534
|
}
|
520
|
-
|
535
|
+
|
521
536
|
exec_file = self.export_directory / f"executive_report_{timestamp}.json"
|
522
|
-
with open(exec_file,
|
537
|
+
with open(exec_file, "w") as f:
|
523
538
|
json.dump(executive_report, f, indent=2, default=str)
|
524
|
-
exported_files[
|
525
|
-
|
539
|
+
exported_files["executive_report"] = str(exec_file)
|
540
|
+
|
526
541
|
# 2. Business Recommendations CSV
|
527
542
|
if self.business_recommendations:
|
528
543
|
recommendations_data = []
|
529
544
|
for rec in self.business_recommendations:
|
530
|
-
recommendations_data.append(
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
545
|
+
recommendations_data.append(
|
546
|
+
{
|
547
|
+
"Title": rec.title,
|
548
|
+
"Monthly Savings": rec.monthly_savings,
|
549
|
+
"Annual Impact": rec.annual_impact,
|
550
|
+
"Priority": rec.business_priority.value,
|
551
|
+
"Risk Level": rec.risk_level.value,
|
552
|
+
"Timeline": rec.implementation_timeline,
|
553
|
+
"Quick Win": rec.quick_win,
|
554
|
+
"Approval Required": rec.approval_required,
|
555
|
+
"Strategic Value": rec.strategic_value,
|
556
|
+
}
|
557
|
+
)
|
558
|
+
|
542
559
|
rec_df = pd.DataFrame(recommendations_data)
|
543
560
|
rec_file = self.export_directory / f"business_recommendations_{timestamp}.csv"
|
544
561
|
rec_df.to_csv(rec_file, index=False)
|
545
|
-
exported_files[
|
546
|
-
|
562
|
+
exported_files["recommendations"] = str(rec_file)
|
563
|
+
|
547
564
|
# 3. Financial Analysis Spreadsheet
|
548
565
|
financial_data = {
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
566
|
+
"Metric": [
|
567
|
+
"Current Monthly Cost",
|
568
|
+
"Potential Monthly Savings",
|
569
|
+
"Annual Savings",
|
570
|
+
"Implementation Cost",
|
571
|
+
"Net Annual Benefit",
|
572
|
+
"ROI Percentage",
|
573
|
+
"Payback Period (Months)",
|
574
|
+
],
|
575
|
+
"Value": [
|
576
|
+
self.analysis_results["executive_summary"]["current_monthly_cost"],
|
577
|
+
self.analysis_results["executive_summary"]["potential_monthly_savings"],
|
578
|
+
self.analysis_results["executive_summary"]["annual_savings_potential"],
|
579
|
+
self.analysis_results["financial_impact"]["implementation_cost"]["estimated_cost"],
|
580
|
+
self.analysis_results["executive_summary"]["annual_savings_potential"]
|
581
|
+
- self.analysis_results["financial_impact"]["implementation_cost"]["estimated_cost"],
|
582
|
+
self.analysis_results["financial_impact"]["roi_analysis"]["roi_percentage"],
|
583
|
+
self.analysis_results["financial_impact"]["roi_analysis"]["payback_months"],
|
557
584
|
],
|
558
|
-
'Value': [
|
559
|
-
self.analysis_results['executive_summary']['current_monthly_cost'],
|
560
|
-
self.analysis_results['executive_summary']['potential_monthly_savings'],
|
561
|
-
self.analysis_results['executive_summary']['annual_savings_potential'],
|
562
|
-
self.analysis_results['financial_impact']['implementation_cost']['estimated_cost'],
|
563
|
-
self.analysis_results['executive_summary']['annual_savings_potential'] - self.analysis_results['financial_impact']['implementation_cost']['estimated_cost'],
|
564
|
-
self.analysis_results['financial_impact']['roi_analysis']['roi_percentage'],
|
565
|
-
self.analysis_results['financial_impact']['roi_analysis']['payback_months']
|
566
|
-
]
|
567
585
|
}
|
568
|
-
|
586
|
+
|
569
587
|
financial_df = pd.DataFrame(financial_data)
|
570
588
|
financial_file = self.export_directory / f"financial_analysis_{timestamp}.csv"
|
571
589
|
financial_df.to_csv(financial_file, index=False)
|
572
|
-
exported_files[
|
573
|
-
|
590
|
+
exported_files["financial_analysis"] = str(financial_file)
|
591
|
+
|
574
592
|
return exported_files
|
575
|
-
|
593
|
+
|
576
594
|
def display_business_dashboard(self) -> None:
|
577
595
|
"""Display comprehensive business dashboard"""
|
578
|
-
|
596
|
+
|
579
597
|
if not self.analysis_results:
|
580
598
|
self.console.print("[red]❌ No analysis results available[/red]")
|
581
599
|
return
|
582
|
-
|
583
|
-
exec_summary = self.analysis_results[
|
584
|
-
financial = self.analysis_results[
|
585
|
-
|
600
|
+
|
601
|
+
exec_summary = self.analysis_results["executive_summary"]
|
602
|
+
financial = self.analysis_results["financial_impact"]
|
603
|
+
|
586
604
|
# Main dashboard panel
|
587
605
|
dashboard_content = (
|
588
606
|
f"[bold blue]💰 FINANCIAL IMPACT[/bold blue]\n"
|
@@ -601,16 +619,13 @@ class VPCManagerInterface:
|
|
601
619
|
f"Quick Wins: [green]{sum(1 for r in self.business_recommendations if r.quick_win)}[/green]\n"
|
602
620
|
f"Approval Required: [red]{sum(1 for r in self.business_recommendations if r.approval_required)}[/red]"
|
603
621
|
)
|
604
|
-
|
622
|
+
|
605
623
|
dashboard_panel = Panel(
|
606
|
-
dashboard_content,
|
607
|
-
title="VPC Cost Optimization - Business Dashboard",
|
608
|
-
style="white",
|
609
|
-
width=80
|
624
|
+
dashboard_content, title="VPC Cost Optimization - Business Dashboard", style="white", width=80
|
610
625
|
)
|
611
|
-
|
626
|
+
|
612
627
|
self.console.print(dashboard_panel)
|
613
|
-
|
628
|
+
|
614
629
|
# Recommendations table
|
615
630
|
if self.business_recommendations:
|
616
631
|
rec_table = Table(title="Business Recommendations", show_header=True)
|
@@ -620,7 +635,7 @@ class VPCManagerInterface:
|
|
620
635
|
rec_table.add_column("Risk", style="red")
|
621
636
|
rec_table.add_column("Timeline", style="blue")
|
622
637
|
rec_table.add_column("Quick Win", style="magenta")
|
623
|
-
|
638
|
+
|
624
639
|
for rec in self.business_recommendations:
|
625
640
|
rec_table.add_row(
|
626
641
|
rec.title,
|
@@ -628,7 +643,7 @@ class VPCManagerInterface:
|
|
628
643
|
rec.business_priority.value,
|
629
644
|
rec.risk_level.value,
|
630
645
|
rec.implementation_timeline,
|
631
|
-
"✅" if rec.quick_win else "⏳"
|
646
|
+
"✅" if rec.quick_win else "⏳",
|
632
647
|
)
|
633
|
-
|
634
|
-
self.console.print(rec_table)
|
648
|
+
|
649
|
+
self.console.print(rec_table)
|