dayhoff-tools 1.1.10__py3-none-any.whl → 1.13.12__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.
Files changed (41) hide show
  1. dayhoff_tools/__init__.py +10 -0
  2. dayhoff_tools/cli/cloud_commands.py +179 -43
  3. dayhoff_tools/cli/engine1/__init__.py +323 -0
  4. dayhoff_tools/cli/engine1/engine_core.py +703 -0
  5. dayhoff_tools/cli/engine1/engine_lifecycle.py +136 -0
  6. dayhoff_tools/cli/engine1/engine_maintenance.py +431 -0
  7. dayhoff_tools/cli/engine1/engine_management.py +505 -0
  8. dayhoff_tools/cli/engine1/shared.py +501 -0
  9. dayhoff_tools/cli/engine1/studio_commands.py +825 -0
  10. dayhoff_tools/cli/engines_studios/__init__.py +6 -0
  11. dayhoff_tools/cli/engines_studios/api_client.py +351 -0
  12. dayhoff_tools/cli/engines_studios/auth.py +144 -0
  13. dayhoff_tools/cli/engines_studios/engine-studio-cli.md +1230 -0
  14. dayhoff_tools/cli/engines_studios/engine_commands.py +1151 -0
  15. dayhoff_tools/cli/engines_studios/progress.py +260 -0
  16. dayhoff_tools/cli/engines_studios/simulators/cli-simulators.md +151 -0
  17. dayhoff_tools/cli/engines_studios/simulators/demo.sh +75 -0
  18. dayhoff_tools/cli/engines_studios/simulators/engine_list_simulator.py +319 -0
  19. dayhoff_tools/cli/engines_studios/simulators/engine_status_simulator.py +369 -0
  20. dayhoff_tools/cli/engines_studios/simulators/idle_status_simulator.py +476 -0
  21. dayhoff_tools/cli/engines_studios/simulators/simulator_utils.py +180 -0
  22. dayhoff_tools/cli/engines_studios/simulators/studio_list_simulator.py +374 -0
  23. dayhoff_tools/cli/engines_studios/simulators/studio_status_simulator.py +164 -0
  24. dayhoff_tools/cli/engines_studios/studio_commands.py +755 -0
  25. dayhoff_tools/cli/main.py +106 -7
  26. dayhoff_tools/cli/utility_commands.py +896 -179
  27. dayhoff_tools/deployment/base.py +70 -6
  28. dayhoff_tools/deployment/deploy_aws.py +165 -25
  29. dayhoff_tools/deployment/deploy_gcp.py +78 -5
  30. dayhoff_tools/deployment/deploy_utils.py +20 -7
  31. dayhoff_tools/deployment/job_runner.py +9 -4
  32. dayhoff_tools/deployment/processors.py +230 -418
  33. dayhoff_tools/deployment/swarm.py +47 -12
  34. dayhoff_tools/embedders.py +28 -26
  35. dayhoff_tools/fasta.py +181 -64
  36. dayhoff_tools/warehouse.py +268 -1
  37. {dayhoff_tools-1.1.10.dist-info → dayhoff_tools-1.13.12.dist-info}/METADATA +20 -5
  38. dayhoff_tools-1.13.12.dist-info/RECORD +54 -0
  39. {dayhoff_tools-1.1.10.dist-info → dayhoff_tools-1.13.12.dist-info}/WHEEL +1 -1
  40. dayhoff_tools-1.1.10.dist-info/RECORD +0 -32
  41. {dayhoff_tools-1.1.10.dist-info → dayhoff_tools-1.13.12.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,476 @@
1
+ #!/usr/bin/env python3
2
+ """Simulator for engine status output - iterate on design locally without AWS.
3
+
4
+ This lets you quickly see how the status command output looks under different
5
+ engine states and sensor combinations.
6
+
7
+ Usage:
8
+ python dayhoff_tools/cli/engines_studios/idle_status_simulator.py # Show all scenarios
9
+ python dayhoff_tools/cli/engines_studios/idle_status_simulator.py --scenario idle # Show specific scenario
10
+ python dayhoff_tools/cli/engines_studios/idle_status_simulator.py --colorful # Use more emojis
11
+ """
12
+
13
+ import argparse
14
+ import sys
15
+ from datetime import datetime, timedelta, timezone
16
+
17
+ # Import standalone utilities
18
+ from pathlib import Path
19
+
20
+ simulator_dir = Path(__file__).parent
21
+ sys.path.insert(0, str(simulator_dir))
22
+ from simulator_utils import format_idle_state, format_time_ago
23
+
24
+
25
+ def generate_scenarios():
26
+ """Generate various test scenarios for status output."""
27
+
28
+ scenarios = {}
29
+
30
+ # Scenario 1: Completely idle engine
31
+ scenarios["idle"] = {
32
+ "name": "Completely Idle Engine",
33
+ "status_data": {
34
+ "name": "alice-work",
35
+ "instance_id": "i-0123456789abcdef0",
36
+ "instance_type": "t3a.xlarge",
37
+ "state": "running",
38
+ "public_ip": "54.123.45.67",
39
+ "launch_time": (
40
+ datetime.now(timezone.utc) - timedelta(hours=2)
41
+ ).isoformat(),
42
+ "idle_state": {
43
+ "is_idle": True,
44
+ "reason": "All sensors report idle",
45
+ "idle_seconds": 450,
46
+ "timeout_seconds": 1800,
47
+ "sensors": {
48
+ "coffee": {
49
+ "active": False,
50
+ "confidence": "HIGH",
51
+ "reason": "No coffee lock",
52
+ "details": {},
53
+ },
54
+ "ssh": {
55
+ "active": False,
56
+ "confidence": "HIGH",
57
+ "reason": "No active SSH sessions",
58
+ "details": {},
59
+ },
60
+ "ide": {
61
+ "active": False,
62
+ "confidence": "MEDIUM",
63
+ "reason": "No IDE connections detected (after 3 checks)",
64
+ "details": {},
65
+ },
66
+ "docker": {
67
+ "active": False,
68
+ "confidence": "MEDIUM",
69
+ "reason": "No workload containers",
70
+ "details": {"ignored": []}, # Empty list - should not display
71
+ },
72
+ },
73
+ },
74
+ "attached_studios": [],
75
+ },
76
+ }
77
+
78
+ # Scenario 2: Active with SSH and Docker
79
+ scenarios["active_ssh_docker"] = {
80
+ "name": "Active: SSH + Docker Workloads",
81
+ "status_data": {
82
+ "name": "bob-training",
83
+ "instance_id": "i-0fedcba987654321",
84
+ "instance_type": "g5.4xlarge",
85
+ "state": "running",
86
+ "public_ip": "34.234.56.78",
87
+ "launch_time": (
88
+ datetime.now(timezone.utc) - timedelta(minutes=45)
89
+ ).isoformat(),
90
+ "idle_state": {
91
+ "is_idle": False,
92
+ "reason": "ssh: 2 SSH session(s), docker: 3 workload container(s)",
93
+ "idle_seconds": 0,
94
+ "timeout_seconds": 1800,
95
+ "sensors": {
96
+ "coffee": {
97
+ "active": False,
98
+ "confidence": "HIGH",
99
+ "reason": "No coffee lock",
100
+ "details": {},
101
+ },
102
+ "ssh": {
103
+ "active": True,
104
+ "confidence": "HIGH",
105
+ "reason": "2 SSH session(s)",
106
+ "details": {
107
+ "sessions": [
108
+ "bob pts/0 2025-11-13 14:30 old 12345",
109
+ "alice pts/1 2025-11-13 15:00 old 67890",
110
+ ]
111
+ },
112
+ },
113
+ "ide": {
114
+ "active": False,
115
+ "confidence": "MEDIUM",
116
+ "reason": "No IDE connections detected",
117
+ "details": {},
118
+ },
119
+ "docker": {
120
+ "active": True,
121
+ "confidence": "MEDIUM",
122
+ "reason": "3 workload container(s)",
123
+ "details": {
124
+ "containers": [
125
+ "training-job-1",
126
+ "tensorboard",
127
+ "jupyter-lab",
128
+ ],
129
+ "ignored": [
130
+ "ecs-agent (AWS system container)",
131
+ "devcontainer (dev-container)",
132
+ ],
133
+ },
134
+ },
135
+ },
136
+ },
137
+ "attached_studios": [{"user": "bob", "studio_id": "vol-0123456789abcdef0"}],
138
+ },
139
+ }
140
+
141
+ # Scenario 3: Active with IDE only
142
+ scenarios["active_ide"] = {
143
+ "name": "Active: IDE Connection Only",
144
+ "status_data": {
145
+ "name": "charlie-dev",
146
+ "instance_id": "i-0abc123def456789",
147
+ "instance_type": "t3a.xlarge",
148
+ "state": "running",
149
+ "public_ip": "52.12.34.56",
150
+ "launch_time": (
151
+ datetime.now(timezone.utc) - timedelta(hours=5)
152
+ ).isoformat(),
153
+ "idle_state": {
154
+ "is_idle": False,
155
+ "reason": "ide: 2 IDE(s):",
156
+ "idle_seconds": 0,
157
+ "timeout_seconds": 1800,
158
+ "sensors": {
159
+ "coffee": {
160
+ "active": False,
161
+ "confidence": "HIGH",
162
+ "reason": "No coffee lock",
163
+ "details": {},
164
+ },
165
+ "ssh": {
166
+ "active": False,
167
+ "confidence": "HIGH",
168
+ "reason": "No SSH sessions",
169
+ "details": {},
170
+ },
171
+ "ide": {
172
+ "active": True,
173
+ "confidence": "MEDIUM",
174
+ "reason": "2 IDE(s):",
175
+ "details": {
176
+ "unique_flavor_count": 2,
177
+ "unique_pid_count": 3,
178
+ "flavors": ["cursor", "vscode"],
179
+ "connections": [
180
+ "PID 12345: node",
181
+ "PID 12346: code-server",
182
+ "PID 12347: cursor-server",
183
+ ],
184
+ },
185
+ },
186
+ "docker": {
187
+ "active": False,
188
+ "confidence": "MEDIUM",
189
+ "reason": "No workload containers",
190
+ "details": {"ignored": ["ecs-agent (AWS system container)"]},
191
+ },
192
+ },
193
+ },
194
+ "attached_studios": [
195
+ {"user": "charlie", "studio_id": "vol-09876543210fedcba"}
196
+ ],
197
+ },
198
+ }
199
+
200
+ # Scenario 4: Coffee lock active (near timeout)
201
+ scenarios["coffee_lock"] = {
202
+ "name": "Active: Coffee Lock (Near Timeout)",
203
+ "status_data": {
204
+ "name": "diana-batch",
205
+ "instance_id": "i-0def789abc123456",
206
+ "instance_type": "c5.9xlarge",
207
+ "state": "running",
208
+ "public_ip": "18.234.56.78",
209
+ "launch_time": (
210
+ datetime.now(timezone.utc) - timedelta(hours=8)
211
+ ).isoformat(),
212
+ "idle_state": {
213
+ "is_idle": False,
214
+ "reason": "coffee: Coffee lock active (15m remaining)",
215
+ "idle_seconds": 0,
216
+ "timeout_seconds": 1800,
217
+ "sensors": {
218
+ "coffee": {
219
+ "active": True,
220
+ "confidence": "HIGH",
221
+ "reason": "Coffee lock active (15m remaining)",
222
+ "details": {
223
+ "expires_at": (datetime.now().timestamp() + 900) # 15 min
224
+ },
225
+ },
226
+ "ssh": {
227
+ "active": False,
228
+ "confidence": "HIGH",
229
+ "reason": "No SSH sessions",
230
+ "details": {},
231
+ },
232
+ "ide": {
233
+ "active": False,
234
+ "confidence": "MEDIUM",
235
+ "reason": "No IDE connections detected",
236
+ "details": {},
237
+ },
238
+ "docker": {
239
+ "active": False,
240
+ "confidence": "MEDIUM",
241
+ "reason": "No workload containers",
242
+ "details": {},
243
+ },
244
+ },
245
+ },
246
+ "attached_studios": [],
247
+ },
248
+ }
249
+
250
+ # Scenario 5: Almost timed out
251
+ scenarios["near_timeout"] = {
252
+ "name": "Nearly Timed Out (28 min idle)",
253
+ "status_data": {
254
+ "name": "eve-forgotten",
255
+ "instance_id": "i-0eeeeeeeeeeeeeeee",
256
+ "instance_type": "t3a.xlarge",
257
+ "state": "running",
258
+ "public_ip": "54.234.56.89",
259
+ "launch_time": (
260
+ datetime.now(timezone.utc) - timedelta(hours=3)
261
+ ).isoformat(),
262
+ "idle_state": {
263
+ "is_idle": True,
264
+ "reason": "All sensors report idle",
265
+ "idle_seconds": 1680, # 28 minutes
266
+ "timeout_seconds": 1800, # 30 minutes
267
+ "sensors": {
268
+ "coffee": {
269
+ "active": False,
270
+ "confidence": "HIGH",
271
+ "reason": "No coffee lock",
272
+ "details": {},
273
+ },
274
+ "ssh": {
275
+ "active": False,
276
+ "confidence": "HIGH",
277
+ "reason": "No SSH sessions",
278
+ "details": {},
279
+ },
280
+ "ide": {
281
+ "active": False,
282
+ "confidence": "MEDIUM",
283
+ "reason": "No IDE connections detected",
284
+ "details": {},
285
+ },
286
+ "docker": {
287
+ "active": False,
288
+ "confidence": "MEDIUM",
289
+ "reason": "No workload containers",
290
+ "details": {},
291
+ },
292
+ },
293
+ },
294
+ "attached_studios": [{"user": "eve", "studio_id": "vol-0eeeeeeeeeeeeeeee"}],
295
+ },
296
+ }
297
+
298
+ # Scenario 6: Initializing (not ready yet)
299
+ scenarios["initializing"] = {
300
+ "name": "Engine Initializing (Not Ready)",
301
+ "status_data": {
302
+ "name": "frank-new",
303
+ "instance_id": "i-0fffffffffffffff",
304
+ "instance_type": "g5.xlarge",
305
+ "state": "running",
306
+ "public_ip": "3.123.45.67",
307
+ "launch_time": (
308
+ datetime.now(timezone.utc) - timedelta(minutes=2)
309
+ ).isoformat(),
310
+ "readiness": {
311
+ "ready": False,
312
+ "status": "configuring",
313
+ "current_stage": "installing_packages",
314
+ "progress_percent": 50,
315
+ "estimated_time_remaining_seconds": 90,
316
+ },
317
+ "idle_state": None, # Not available yet
318
+ "attached_studios": [],
319
+ },
320
+ }
321
+
322
+ # Scenario 7: Stopped engine
323
+ scenarios["stopped"] = {
324
+ "name": "Stopped Engine",
325
+ "status_data": {
326
+ "name": "george-stopped",
327
+ "instance_id": "i-0aaaaaaaaaaaaaaaa",
328
+ "instance_type": "t3a.xlarge",
329
+ "state": "stopped",
330
+ "public_ip": None,
331
+ "launch_time": (
332
+ datetime.now(timezone.utc) - timedelta(hours=4)
333
+ ).isoformat(),
334
+ "idle_state": {
335
+ "is_idle": True,
336
+ "reason": "All sensors report idle",
337
+ "idle_seconds": 1800,
338
+ "timeout_seconds": 1800,
339
+ "sensors": {
340
+ "coffee": {
341
+ "active": False,
342
+ "confidence": "HIGH",
343
+ "reason": "No coffee lock",
344
+ "details": {},
345
+ },
346
+ "ssh": {
347
+ "active": False,
348
+ "confidence": "HIGH",
349
+ "reason": "No SSH sessions",
350
+ "details": {},
351
+ },
352
+ "ide": {
353
+ "active": False,
354
+ "confidence": "MEDIUM",
355
+ "reason": "No IDE connections",
356
+ "details": {},
357
+ },
358
+ "docker": {
359
+ "active": False,
360
+ "confidence": "MEDIUM",
361
+ "reason": "No workload containers",
362
+ "details": {},
363
+ },
364
+ },
365
+ },
366
+ "attached_studios": [],
367
+ },
368
+ }
369
+
370
+ return scenarios
371
+
372
+
373
+ def display_scenario(name, scenario_data, detailed=True):
374
+ """Display a single scenario in formatted output."""
375
+ status = scenario_data["status_data"]
376
+
377
+ print(f"\n{'='*80}")
378
+ print(f" SCENARIO: {scenario_data['name']}")
379
+ print(f"{'='*80}\n")
380
+
381
+ # Basic info
382
+ print(f"Engine: \033[34m{status['name']}\033[0m") # Blue engine name
383
+ print(f"Instance ID: {status['instance_id']}")
384
+ print(f"Type: {status['instance_type']}")
385
+
386
+ # Show state in red if stopped, normal otherwise
387
+ engine_state = status["state"]
388
+ if engine_state.lower() in ["stopped", "stopping", "terminated", "terminating"]:
389
+ print(f"State: \033[31m{engine_state}\033[0m") # Red for stopped
390
+ else:
391
+ print(f"State: {engine_state}")
392
+
393
+ if status.get("public_ip"):
394
+ print(f"Public IP: {status['public_ip']}")
395
+
396
+ if status.get("launch_time"):
397
+ print(f"Launched: {format_time_ago(status['launch_time'])}")
398
+
399
+ # Check if engine is stopped - don't show idle state or activity sensors
400
+ if engine_state.lower() in ["stopped", "stopping", "terminated", "terminating"]:
401
+ print() # Extra newline for readability
402
+ return
403
+
404
+ # Show readiness if not ready
405
+ if status.get("readiness") and not status["readiness"].get("ready"):
406
+ readiness = status["readiness"]
407
+ print(f"\n⏳ Initialization: {readiness.get('progress_percent', 0)}%")
408
+ print(f"Current Stage: {readiness.get('current_stage', 'unknown')}")
409
+ if readiness.get("estimated_time_remaining_seconds"):
410
+ remaining = readiness["estimated_time_remaining_seconds"]
411
+ print(f"Estimated Time Remaining: {remaining}s")
412
+
413
+ # Show idle state (only for running engines)
414
+ if status.get("idle_state"):
415
+ attached_studios = status.get("attached_studios", [])
416
+ print(
417
+ f"\n{format_idle_state(status['idle_state'], detailed=detailed, attached_studios=attached_studios)}"
418
+ )
419
+
420
+ print() # Extra newline for readability
421
+
422
+
423
+ def main():
424
+ parser = argparse.ArgumentParser(
425
+ description="Simulator for engine status output design iteration"
426
+ )
427
+ parser.add_argument(
428
+ "--scenario",
429
+ help="Show specific scenario (idle, active_ssh_docker, active_ide, coffee_lock, near_timeout, initializing, stopped)",
430
+ type=str,
431
+ )
432
+ parser.add_argument(
433
+ "--simple", action="store_true", help="Show simple (non-detailed) output"
434
+ )
435
+ parser.add_argument(
436
+ "--colorful",
437
+ action="store_true",
438
+ help="Use more emojis and colors (future: implement enhanced formatting)",
439
+ )
440
+
441
+ args = parser.parse_args()
442
+
443
+ scenarios = generate_scenarios()
444
+
445
+ if args.scenario:
446
+ if args.scenario not in scenarios:
447
+ print(f"❌ Unknown scenario: {args.scenario}")
448
+ print(f"Available scenarios: {', '.join(scenarios.keys())}")
449
+ return 1
450
+
451
+ display_scenario(
452
+ args.scenario, scenarios[args.scenario], detailed=not args.simple
453
+ )
454
+ else:
455
+ # Show all scenarios
456
+ print("\n" + "=" * 80)
457
+ print(" ENGINE STATUS OUTPUT SIMULATOR")
458
+ print("=" * 80)
459
+ print("\nShowing all scenarios. Use --scenario <name> to see just one.")
460
+ print("Use --simple to see non-detailed output.")
461
+ print()
462
+
463
+ for name, scenario_data in scenarios.items():
464
+ display_scenario(name, scenario_data, detailed=not args.simple)
465
+
466
+ if args.colorful:
467
+ print("\n💡 TIP: --colorful flag noted! Implement enhanced formatting by:")
468
+ print(" 1. Edit format_idle_state() in progress.py")
469
+ print(" 2. Re-run this simulator to see changes")
470
+ print(" 3. No AWS calls needed - iterate quickly!\n")
471
+
472
+ return 0
473
+
474
+
475
+ if __name__ == "__main__":
476
+ sys.exit(main())
@@ -0,0 +1,180 @@
1
+ """Standalone utilities for simulators - no external dependencies."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any, Dict, Optional
5
+
6
+
7
+ def format_time_ago(timestamp: str) -> str:
8
+ """Format timestamp as time ago.
9
+
10
+ Args:
11
+ timestamp: ISO format timestamp
12
+
13
+ Returns:
14
+ Human readable time ago string
15
+ """
16
+ try:
17
+ dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
18
+ now = datetime.now(dt.tzinfo)
19
+ delta = now - dt
20
+
21
+ seconds = int(delta.total_seconds())
22
+ if seconds < 60:
23
+ return f"{seconds}s ago"
24
+ elif seconds < 3600:
25
+ return f"{seconds // 60}m ago"
26
+ elif seconds < 86400:
27
+ return f"{seconds // 3600}h ago"
28
+ else:
29
+ return f"{seconds // 86400}d ago"
30
+ except:
31
+ return timestamp
32
+
33
+
34
+ def format_sensor_status(sensor_data: Dict[str, Any]) -> str:
35
+ """Format sensor status for display.
36
+
37
+ Args:
38
+ sensor_data: Sensor data dict
39
+
40
+ Returns:
41
+ Formatted string
42
+ """
43
+ active = sensor_data.get("active", False)
44
+ reason = sensor_data.get("reason", "No reason provided")
45
+
46
+ if active:
47
+ return f"🟢\n {reason}"
48
+ else:
49
+ return "⚪"
50
+
51
+
52
+ def format_idle_state(
53
+ idle_state: Dict[str, Any],
54
+ detailed: bool = True,
55
+ attached_studios: Optional[list] = None,
56
+ ) -> str:
57
+ """Format idle state for display.
58
+
59
+ Args:
60
+ idle_state: Idle state dict
61
+ detailed: Whether to show detailed sensor information
62
+ attached_studios: Optional list of attached studio dicts
63
+
64
+ Returns:
65
+ Formatted string
66
+ """
67
+ is_idle = idle_state.get("is_idle", False)
68
+
69
+ lines = []
70
+
71
+ # Status line
72
+ icon = "🟡 IDLE" if is_idle else "🟢 ACTIVE"
73
+ lines.append(f"Idle Status: {icon}")
74
+
75
+ # Timing information
76
+ if idle_state.get("idle_seconds"):
77
+ timeout = int(idle_state.get("timeout_seconds", 1800))
78
+ elapsed = idle_state["idle_seconds"]
79
+ remaining = max(0, timeout - elapsed)
80
+ lines.append(f"Idle Time: {elapsed}s / {timeout}s")
81
+ if remaining > 0:
82
+ minutes = remaining // 60
83
+ # Yellow text using ANSI escape codes
84
+ lines.append(f"\033[33mWill shutdown in: {remaining}s ({minutes}m)\033[0m")
85
+
86
+ # Attached studios (show before sensors)
87
+ if attached_studios:
88
+ # Purple text for studio names
89
+ studio_names = ", ".join(
90
+ [
91
+ f"\033[35m{s.get('user', s.get('studio_id', 'unknown'))}\033[0m"
92
+ for s in attached_studios
93
+ ]
94
+ )
95
+ lines.append(f"\nAttached Studios: {studio_names}")
96
+ else:
97
+ # Normal text for "None"
98
+ lines.append(f"\nAttached Studios: None")
99
+
100
+ # Detailed sensor information with colorful emojis
101
+ if detailed and idle_state.get("sensors"):
102
+ lines.append(f"\n{'═'*60}")
103
+ lines.append("🔍 Activity Sensors:")
104
+ lines.append(f"{'═'*60}")
105
+
106
+ # Sensor emoji mapping
107
+ sensor_emojis = {
108
+ "coffee": "☕",
109
+ "ssh": "🐚",
110
+ "ide": "💻",
111
+ "docker": "🐳",
112
+ }
113
+
114
+ for sensor_name, sensor_data in idle_state["sensors"].items():
115
+ emoji = sensor_emojis.get(sensor_name.lower(), "📊")
116
+ active = sensor_data.get("active", False)
117
+
118
+ # Special formatting for coffee sensor
119
+ if sensor_name.lower() == "coffee" and active:
120
+ # Extract minutes from details for cleaner display
121
+ details = sensor_data.get("details", {})
122
+ remaining_seconds = int(details.get("remaining_seconds", 0))
123
+ remaining_minutes = remaining_seconds // 60
124
+ lines.append(f"\n{emoji} {sensor_name.upper()} 🟢")
125
+ lines.append(f" Caffeinated for another {remaining_minutes}m")
126
+ else:
127
+ status_icon = format_sensor_status(sensor_data)
128
+ # Format: emoji NAME status_icon (on same line for inactive, split for active)
129
+ if active:
130
+ lines.append(
131
+ f"\n{emoji} {sensor_name.upper()} {status_icon.split(chr(10))[0]}"
132
+ )
133
+ # Add reason on next line
134
+ reason_line = (
135
+ status_icon.split("\n")[1] if "\n" in status_icon else ""
136
+ )
137
+ if reason_line:
138
+ lines.append(reason_line)
139
+ else:
140
+ lines.append(f"\n{emoji} {sensor_name.upper()} {status_icon}")
141
+
142
+ # Show details if available (skip for active coffee sensor with special formatting)
143
+ if sensor_name.lower() == "coffee" and active:
144
+ continue # Already showed coffee details in special format above
145
+
146
+ details = sensor_data.get("details", {})
147
+ if details:
148
+ for key, value in details.items():
149
+ # Skip internal bookkeeping fields and redundant info
150
+ if key in [
151
+ "unique_flavor_count",
152
+ "unique_pid_count",
153
+ "expires_at",
154
+ "flavors",
155
+ "remaining_seconds", # Redundant with time shown in reason
156
+ "pid_count", # Redundant with connections list
157
+ "connections", # Redundant, connections shown in sessions/containers
158
+ ]:
159
+ continue
160
+
161
+ if isinstance(value, list):
162
+ if value: # Only show non-empty lists
163
+ # All lists shown with bullets, no header for containers/connections/sessions
164
+ if key in ["containers", "connections", "sessions"]:
165
+ # Just show the items with bullets, no header
166
+ for item in value[:5]:
167
+ lines.append(f" • {item}")
168
+ elif key == "ignored":
169
+ # Ignored list at same indentation level, with header
170
+ lines.append(f" {key}:")
171
+ for item in value[:5]:
172
+ lines.append(f" • {item}")
173
+ else:
174
+ # Other lists get shown with bullets only
175
+ for item in value[:5]:
176
+ lines.append(f" • {item}")
177
+ elif not isinstance(value, (dict, list)):
178
+ lines.append(f" ℹ️ {key}: {value}")
179
+
180
+ return "\n".join(lines)