kubsome 1.0.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.
Files changed (128) hide show
  1. api/__init__.py +0 -0
  2. api/app.py +48 -0
  3. api/routes/__init__.py +0 -0
  4. api/routes/contexts.py +77 -0
  5. api/routes/deployments.py +62 -0
  6. api/routes/diagnostics.py +44 -0
  7. api/routes/events.py +16 -0
  8. api/routes/intelligence.py +63 -0
  9. api/routes/logs.py +36 -0
  10. api/routes/metrics.py +23 -0
  11. api/routes/operations.py +279 -0
  12. api/routes/overview.py +105 -0
  13. api/routes/pods.py +16 -0
  14. api/routes/terminal.py +182 -0
  15. api/routes/ws.py +114 -0
  16. api/serve.py +17 -0
  17. config/__init__.py +0 -0
  18. config/settings.py +3 -0
  19. core/__init__.py +0 -0
  20. core/ai/__init__.py +0 -0
  21. core/ai/anomaly.py +227 -0
  22. core/ai/correlation.py +124 -0
  23. core/ai/engine.py +646 -0
  24. core/ai/explain.py +180 -0
  25. core/ai/generator.py +129 -0
  26. core/ai/llm.py +102 -0
  27. core/ai/nlp.py +120 -0
  28. core/ai/playbooks.py +194 -0
  29. core/ai/suggest.py +49 -0
  30. core/analyzer.py +58 -0
  31. core/audit.py +47 -0
  32. core/banner.py +64 -0
  33. core/bookmarks.py +59 -0
  34. core/chaining.py +24 -0
  35. core/collectors/__init__.py +0 -0
  36. core/collectors/changes.py +217 -0
  37. core/collectors/configs.py +81 -0
  38. core/collectors/cost.py +318 -0
  39. core/collectors/deployments.py +46 -0
  40. core/collectors/diagnosis.py +27 -0
  41. core/collectors/diff.py +178 -0
  42. core/collectors/events.py +47 -0
  43. core/collectors/image_pull.py +175 -0
  44. core/collectors/inspect.py +173 -0
  45. core/collectors/jobs.py +120 -0
  46. core/collectors/labels.py +76 -0
  47. core/collectors/logs.py +224 -0
  48. core/collectors/metrics.py +119 -0
  49. core/collectors/multicluster.py +97 -0
  50. core/collectors/namespace.py +76 -0
  51. core/collectors/network.py +145 -0
  52. core/collectors/nodes.py +52 -0
  53. core/collectors/pods.py +52 -0
  54. core/collectors/rbac.py +94 -0
  55. core/collectors/rollouts.py +133 -0
  56. core/collectors/scaling.py +331 -0
  57. core/collectors/search.py +66 -0
  58. core/collectors/security.py +181 -0
  59. core/collectors/services.py +293 -0
  60. core/collectors/timeline.py +68 -0
  61. core/collectors/trace.py +231 -0
  62. core/commands.py +585 -0
  63. core/completer.py +240 -0
  64. core/config.py +98 -0
  65. core/context.py +17 -0
  66. core/context_formatter.py +38 -0
  67. core/context_switcher.py +61 -0
  68. core/diagnostics/__init__.py +0 -0
  69. core/diagnostics/engine.py +260 -0
  70. core/diagnostics/recommendations.py +38 -0
  71. core/dispatcher.py +929 -0
  72. core/executor.py +21 -0
  73. core/export.py +150 -0
  74. core/formatter.py +134 -0
  75. core/health.py +75 -0
  76. core/healthcheck.py +120 -0
  77. core/history.py +12 -0
  78. core/incident/__init__.py +0 -0
  79. core/incident/manager.py +162 -0
  80. core/insights.py +14 -0
  81. core/k8s.py +67 -0
  82. core/kubeconfig.py +79 -0
  83. core/notify.py +60 -0
  84. core/overview_formatter.py +162 -0
  85. core/plugins.py +85 -0
  86. core/pod_actions.py +135 -0
  87. core/renderers/__init__.py +0 -0
  88. core/renderers/ai_renderer.py +26 -0
  89. core/renderers/anomaly_renderer.py +135 -0
  90. core/renderers/changes_renderer.py +135 -0
  91. core/renderers/compare_renderer.py +91 -0
  92. core/renderers/cost_renderer.py +174 -0
  93. core/renderers/diagnosis_renderer.py +110 -0
  94. core/renderers/events_renderer.py +127 -0
  95. core/renderers/help_renderer.py +165 -0
  96. core/renderers/incident_renderer.py +92 -0
  97. core/renderers/inspect_renderer.py +227 -0
  98. core/renderers/logs_renderer.py +186 -0
  99. core/renderers/metrics_renderer.py +136 -0
  100. core/renderers/namespace_renderer.py +82 -0
  101. core/renderers/ops_renderer.py +230 -0
  102. core/renderers/rbac_renderer.py +135 -0
  103. core/renderers/report_renderer.py +111 -0
  104. core/renderers/rollout_renderer.py +123 -0
  105. core/renderers/scaling_renderer.py +236 -0
  106. core/renderers/search_renderer.py +59 -0
  107. core/renderers/services_renderer.py +169 -0
  108. core/renderers/trace_renderer.py +105 -0
  109. core/renderers/workflow_renderer.py +68 -0
  110. core/resolver.py +94 -0
  111. core/safety.py +16 -0
  112. core/selector.py +77 -0
  113. core/spinner.py +17 -0
  114. core/state.py +28 -0
  115. core/theme.py +68 -0
  116. core/watch_formatter.py +116 -0
  117. core/workflows.py +112 -0
  118. kubsome-1.0.0.dist-info/METADATA +186 -0
  119. kubsome-1.0.0.dist-info/RECORD +128 -0
  120. kubsome-1.0.0.dist-info/WHEEL +5 -0
  121. kubsome-1.0.0.dist-info/entry_points.txt +2 -0
  122. kubsome-1.0.0.dist-info/licenses/LICENSE +21 -0
  123. kubsome-1.0.0.dist-info/top_level.txt +6 -0
  124. main.py +296 -0
  125. plugins/__init__.py +0 -0
  126. plugins/example_health.py +27 -0
  127. tui/__init__.py +0 -0
  128. tui/app.py +296 -0
api/__init__.py ADDED
File without changes
api/app.py ADDED
@@ -0,0 +1,48 @@
1
+ """
2
+ Kubsome API — FastAPI backend exposing the Kubernetes engine.
3
+ """
4
+
5
+ from fastapi import FastAPI
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from fastapi.staticfiles import StaticFiles
8
+ from pathlib import Path
9
+
10
+ from api.routes import pods, overview, contexts, events, metrics, logs, deployments, diagnostics, intelligence, terminal, operations, ws
11
+
12
+ app = FastAPI(
13
+ title="Kubsome API",
14
+ version="1.0.0",
15
+ description="Kubernetes Operations Engine API",
16
+ )
17
+
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=["http://localhost:3000", "http://localhost:3001"],
21
+ allow_credentials=True,
22
+ allow_methods=["*"],
23
+ allow_headers=["*"],
24
+ )
25
+
26
+ app.include_router(pods.router, prefix="/api")
27
+ app.include_router(overview.router, prefix="/api")
28
+ app.include_router(contexts.router, prefix="/api")
29
+ app.include_router(events.router, prefix="/api")
30
+ app.include_router(metrics.router, prefix="/api")
31
+ app.include_router(logs.router, prefix="/api")
32
+ app.include_router(deployments.router, prefix="/api")
33
+ app.include_router(diagnostics.router, prefix="/api")
34
+ app.include_router(intelligence.router, prefix="/api")
35
+ app.include_router(terminal.router, prefix="/api")
36
+ app.include_router(operations.router, prefix="/api")
37
+ app.include_router(ws.router)
38
+
39
+
40
+ @app.get("/health")
41
+ def health():
42
+ return {"status": "ok"}
43
+
44
+
45
+ # Serve Angular build in production
46
+ ui_dist = Path(__file__).parent.parent / "ui" / "dist" / "ui" / "browser"
47
+ if ui_dist.exists():
48
+ app.mount("/", StaticFiles(directory=str(ui_dist), html=True), name="ui")
api/routes/__init__.py ADDED
File without changes
api/routes/contexts.py ADDED
@@ -0,0 +1,77 @@
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+
4
+ from core.kubeconfig import enriched_contexts
5
+ from core.context_switcher import find_context, switch_context
6
+ from core.context import context
7
+
8
+ router = APIRouter(tags=["contexts"])
9
+
10
+
11
+ class SwitchRequest(BaseModel):
12
+ name: str
13
+
14
+
15
+ @router.get("/contexts")
16
+ def get_contexts():
17
+ return {
18
+ "current": context.current_context,
19
+ "namespace": context.namespace,
20
+ "contexts": enriched_contexts(),
21
+ }
22
+
23
+
24
+ @router.post("/switch-context")
25
+ def post_switch_context(req: SwitchRequest):
26
+ matches = find_context(req.name)
27
+ if not matches:
28
+ raise HTTPException(status_code=404, detail="No matching context")
29
+
30
+ target = matches[0]
31
+ switch_context(target)
32
+ return {
33
+ "switched_to": target["name"],
34
+ "namespace": target["namespace"],
35
+ "environment": target.get("environment"),
36
+ }
37
+
38
+
39
+ class NamespaceRequest(BaseModel):
40
+ namespace: str
41
+
42
+
43
+ @router.get("/namespaces")
44
+ def get_namespaces():
45
+ import subprocess
46
+ result = subprocess.run(
47
+ f"kubectl --context {context.current_context} get namespaces -o jsonpath='{{.items[*].metadata.name}}'",
48
+ shell=True, capture_output=True, text=True,
49
+ )
50
+ if result.returncode != 0:
51
+ return {"namespaces": [], "current": context.namespace}
52
+ raw = result.stdout.strip().strip("'")
53
+ namespaces = sorted([ns for ns in raw.split() if ns])
54
+ return {"namespaces": namespaces, "current": context.namespace}
55
+
56
+
57
+ @router.post("/switch-namespace")
58
+ def post_switch_namespace(req: NamespaceRequest):
59
+ from core.state import save_state
60
+ context.namespace = req.namespace
61
+ save_state(context.current_context, context.namespace)
62
+ return {"namespace": context.namespace}
63
+
64
+
65
+ @router.get("/namespaces/{ctx}")
66
+ def get_namespaces_for(ctx: str):
67
+ """Get namespaces for a specific context without switching global state."""
68
+ import subprocess
69
+ result = subprocess.run(
70
+ f"kubectl --context {ctx} get namespaces -o jsonpath='{{.items[*].metadata.name}}'",
71
+ shell=True, capture_output=True, text=True,
72
+ )
73
+ if result.returncode != 0:
74
+ return {"namespaces": []}
75
+ raw = result.stdout.strip().strip("'")
76
+ namespaces = sorted([ns for ns in raw.split() if ns])
77
+ return {"namespaces": namespaces}
@@ -0,0 +1,62 @@
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+
4
+ from core.context import context
5
+ from core.collectors.deployments import collect_deployments
6
+ from core.collectors.rollouts import (
7
+ rollout_status, rollout_history,
8
+ rollout_rollback, rollout_restart,
9
+ )
10
+
11
+ import subprocess
12
+
13
+ router = APIRouter(tags=["deployments"])
14
+
15
+
16
+ class ScaleRequest(BaseModel):
17
+ replicas: int
18
+
19
+
20
+ @router.get("/deployments")
21
+ def get_deployments():
22
+ return {
23
+ "context": context.current_context,
24
+ "namespace": context.namespace,
25
+ "deployments": collect_deployments(),
26
+ }
27
+
28
+
29
+ @router.get("/rollout/{name}")
30
+ def get_rollout(name: str):
31
+ status = rollout_status(name)
32
+ history = rollout_history(name)
33
+ return {"name": name, "status": status, "history": history}
34
+
35
+
36
+ @router.post("/restart/{name}")
37
+ def post_restart(name: str):
38
+ success, output = rollout_restart(name)
39
+ if not success:
40
+ raise HTTPException(status_code=500, detail="Restart failed")
41
+ return {"restarted": name}
42
+
43
+
44
+ @router.post("/rollback/{name}")
45
+ def post_rollback(name: str):
46
+ success, output = rollout_rollback(name)
47
+ if not success:
48
+ raise HTTPException(status_code=500, detail="Rollback failed")
49
+ return {"rolled_back": name}
50
+
51
+
52
+ @router.post("/scale/{name}")
53
+ def post_scale(name: str, req: ScaleRequest):
54
+ cmd = (
55
+ f"kubectl --context {context.current_context} "
56
+ f"scale deployment/{name} "
57
+ f"--replicas={req.replicas} -n {context.namespace}"
58
+ )
59
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
60
+ if result.returncode != 0:
61
+ raise HTTPException(status_code=500, detail=result.stderr.strip())
62
+ return {"scaled": name, "replicas": req.replicas}
@@ -0,0 +1,44 @@
1
+ from fastapi import APIRouter, HTTPException
2
+
3
+ from core.context import context
4
+ from core.collectors.inspect import inspect_pod, pod_events, extract_pod_details
5
+ from core.collectors.diagnosis import collect_diagnosis
6
+ from core.collectors.trace import trace_resource
7
+ from core.diagnostics.engine import diagnose
8
+ from core.diagnostics.recommendations import recommend
9
+
10
+ router = APIRouter(tags=["diagnostics"])
11
+
12
+
13
+ @router.get("/inspect/{pod}")
14
+ def get_inspect(pod: str):
15
+ pod_data = inspect_pod(pod)
16
+ if not pod_data:
17
+ raise HTTPException(status_code=404, detail="Pod not found")
18
+ details = extract_pod_details(pod_data)
19
+ events = pod_events(pod)
20
+ recommendation = recommend(pod_data)
21
+ return {
22
+ "pod": pod,
23
+ "details": details,
24
+ "events": events,
25
+ "recommendation": recommendation,
26
+ }
27
+
28
+
29
+ @router.get("/diagnose/{pod}")
30
+ def get_diagnose(pod: str):
31
+ data = collect_diagnosis(pod)
32
+ if not data:
33
+ raise HTTPException(status_code=404, detail="Pod not found")
34
+ findings = diagnose(data)
35
+ return {
36
+ "pod": pod,
37
+ "findings": findings,
38
+ }
39
+
40
+
41
+ @router.get("/trace/{name}")
42
+ def get_trace(name: str):
43
+ data = trace_resource(name)
44
+ return {"name": name, "trace": data}
api/routes/events.py ADDED
@@ -0,0 +1,16 @@
1
+ from fastapi import APIRouter
2
+
3
+ from core.collectors.events import collect_events
4
+ from core.context import context
5
+
6
+ router = APIRouter(tags=["events"])
7
+
8
+
9
+ @router.get("/events")
10
+ def get_events(limit: int = 50):
11
+ events = collect_events(limit=limit)
12
+ return {
13
+ "context": context.current_context,
14
+ "namespace": context.namespace,
15
+ "events": events,
16
+ }
@@ -0,0 +1,63 @@
1
+ from fastapi import APIRouter, Query
2
+ from pydantic import BaseModel
3
+
4
+ from core.collectors.search import search_resources
5
+ from core.collectors.security import security_scan
6
+ from core.collectors.cost import resource_recommendations, find_unused_resources
7
+ from core.healthcheck import run_health_check
8
+ from core.ai.engine import handle_ai_query
9
+ from core.ai.anomaly import detect_anomalies
10
+
11
+ router = APIRouter(tags=["intelligence"])
12
+
13
+
14
+ class AiRequest(BaseModel):
15
+ query: str
16
+
17
+
18
+ @router.get("/search")
19
+ def get_search(q: str = Query(..., min_length=1)):
20
+ results = search_resources(q)
21
+ return {"query": q, "results": results}
22
+
23
+
24
+ @router.get("/security")
25
+ def get_security():
26
+ findings = security_scan()
27
+ return {"findings": findings}
28
+
29
+
30
+ @router.get("/health-check")
31
+ def get_health_check():
32
+ return run_health_check()
33
+
34
+
35
+ @router.get("/anomalies")
36
+ def get_anomalies():
37
+ alerts = detect_anomalies()
38
+ return {"alerts": alerts}
39
+
40
+
41
+ @router.get("/optimize")
42
+ def get_optimize():
43
+ recs = resource_recommendations()
44
+ return {"recommendations": recs}
45
+
46
+
47
+ @router.get("/unused")
48
+ def get_unused():
49
+ return {"resources": find_unused_resources()}
50
+
51
+
52
+ @router.post("/ai")
53
+ def post_ai(req: AiRequest):
54
+ import re
55
+ response = handle_ai_query(req.query)
56
+ # Strip Rich markup tags for API consumers
57
+ content = response.get("content", "")
58
+ content = re.sub(r'\[/?[^\]]+\]', '', content)
59
+ return {
60
+ "title": response.get("title", "").replace("🤖 ", ""),
61
+ "answer": content,
62
+ "severity": response.get("severity", "info"),
63
+ }
api/routes/logs.py ADDED
@@ -0,0 +1,36 @@
1
+ from fastapi import APIRouter, Query
2
+ from fastapi.responses import StreamingResponse
3
+
4
+ from core.context import context
5
+ from core.collectors.logs import fetch_logs, stream_logs
6
+
7
+ router = APIRouter(tags=["logs"])
8
+
9
+
10
+ @router.get("/logs/{pod}")
11
+ def get_logs(
12
+ pod: str,
13
+ tail: int = Query(100, ge=1, le=5000),
14
+ errors: bool = False,
15
+ previous: bool = False,
16
+ ):
17
+ lines = fetch_logs(pod, tail=tail, previous=previous, errors_only=errors)
18
+ return {
19
+ "pod": pod,
20
+ "namespace": context.namespace,
21
+ "lines": lines,
22
+ "count": len(lines),
23
+ }
24
+
25
+
26
+ @router.get("/logs/{pod}/stream")
27
+ def get_logs_stream(pod: str):
28
+ def generate():
29
+ process = stream_logs(pod)
30
+ try:
31
+ for line in process.stdout:
32
+ yield line
33
+ finally:
34
+ process.kill()
35
+
36
+ return StreamingResponse(generate(), media_type="text/plain")
api/routes/metrics.py ADDED
@@ -0,0 +1,23 @@
1
+ from fastapi import APIRouter
2
+
3
+ from core.collectors.metrics import top_pods, top_nodes
4
+ from core.context import context
5
+
6
+ router = APIRouter(tags=["metrics"])
7
+
8
+
9
+ @router.get("/top/pods")
10
+ def get_top_pods():
11
+ return {
12
+ "context": context.current_context,
13
+ "namespace": context.namespace,
14
+ "pods": top_pods(),
15
+ }
16
+
17
+
18
+ @router.get("/top/nodes")
19
+ def get_top_nodes():
20
+ return {
21
+ "context": context.current_context,
22
+ "nodes": top_nodes(),
23
+ }
@@ -0,0 +1,279 @@
1
+ import re
2
+ from fastapi import APIRouter, Query
3
+ from pydantic import BaseModel
4
+ from typing import Optional
5
+
6
+ from core.context import context
7
+
8
+ router = APIRouter(tags=["operations"])
9
+
10
+
11
+ # ─── Image Pull Secrets ───────────────────────────────────────────────────────
12
+
13
+ @router.get("/image-pull-secrets")
14
+ def get_image_pull_secrets(pod: Optional[str] = None):
15
+ from core.collectors.image_pull import check_image_pull_secrets
16
+ return check_image_pull_secrets(pod)
17
+
18
+
19
+ # ─── Incident Mode ───────────────────────────────────────────────────────────
20
+
21
+ class IncidentStartRequest(BaseModel):
22
+ title: str = ""
23
+
24
+ class NoteRequest(BaseModel):
25
+ text: str
26
+
27
+ @router.post("/incident/start")
28
+ def incident_start(req: IncidentStartRequest):
29
+ from core.incident.manager import start_incident
30
+ incident = start_incident(req.title)
31
+ return incident
32
+
33
+ @router.post("/incident/stop")
34
+ def incident_stop():
35
+ from core.incident.manager import stop_incident
36
+ result = stop_incident()
37
+ if not result:
38
+ return {"status": "no active incident"}
39
+ incident, path = result
40
+ return {"incident": incident, "export_path": path}
41
+
42
+ @router.get("/incident/status")
43
+ def incident_status():
44
+ from core.incident.manager import get_active
45
+ return get_active() or {"status": "no active incident"}
46
+
47
+ @router.post("/incident/note")
48
+ def incident_note(req: NoteRequest):
49
+ from core.incident.manager import add_note
50
+ if add_note(req.text):
51
+ return {"added": True}
52
+ return {"added": False, "reason": "no active incident"}
53
+
54
+ @router.post("/incident/snapshot")
55
+ def incident_snapshot():
56
+ from core.incident.manager import snapshot
57
+ if snapshot():
58
+ return {"captured": True}
59
+ return {"captured": False, "reason": "no active incident"}
60
+
61
+
62
+ # ─── RBAC ─────────────────────────────────────────────────────────────────────
63
+
64
+ @router.get("/rbac")
65
+ def get_rbac():
66
+ from core.collectors.rbac import list_role_bindings
67
+ return {"bindings": list_role_bindings()}
68
+
69
+
70
+ # ─── CronJobs / Jobs ─────────────────────────────────────────────────────────
71
+
72
+ @router.get("/cronjobs")
73
+ def get_cronjobs():
74
+ from core.collectors.jobs import list_cronjobs
75
+ return {"cronjobs": list_cronjobs()}
76
+
77
+ @router.get("/jobs")
78
+ def get_jobs():
79
+ from core.collectors.jobs import list_jobs
80
+ return {"jobs": list_jobs()}
81
+
82
+ @router.post("/trigger/{name}")
83
+ def trigger_cronjob(name: str):
84
+ from core.collectors.jobs import trigger_cronjob as do_trigger
85
+ success, output = do_trigger(name)
86
+ return {"success": success, "output": output}
87
+
88
+
89
+ # ─── Namespace Overview ───────────────────────────────────────────────────────
90
+
91
+ @router.get("/ns-overview")
92
+ def get_ns_overview():
93
+ from core.collectors.namespace import namespace_summary
94
+ return namespace_summary()
95
+
96
+
97
+ # ─── Compare (Multi-cluster) ─────────────────────────────────────────────────
98
+
99
+ class CompareRequest(BaseModel):
100
+ ctx_a: str
101
+ ctx_b: str
102
+ ns_a: str = "default"
103
+ ns_b: str = "default"
104
+
105
+ @router.post("/compare")
106
+ def post_compare(req: CompareRequest):
107
+ from core.collectors.multicluster import compare_contexts
108
+ data = compare_contexts(req.ctx_a, req.ctx_b, req.ns_a, req.ns_b)
109
+ return data
110
+
111
+
112
+ # ─── Network ─────────────────────────────────────────────────────────────────
113
+
114
+ @router.get("/netcheck/{pod}")
115
+ def get_netcheck(pod: str):
116
+ from core.collectors.network import netcheck
117
+ return netcheck(pod)
118
+
119
+
120
+ # ─── ConfigMap / Secret ───────────────────────────────────────────────────────
121
+
122
+ @router.get("/configmap/{name}")
123
+ def get_configmap(name: str):
124
+ from core.collectors.configs import get_configmap as fetch_cm
125
+ return fetch_cm(name)
126
+
127
+ @router.get("/secret/{name}")
128
+ def get_secret(name: str):
129
+ from core.collectors.configs import get_secret as fetch_secret
130
+ return fetch_secret(name)
131
+
132
+
133
+ # ─── Deployment Diff ──────────────────────────────────────────────────────────
134
+
135
+ @router.get("/diff/{name}")
136
+ def get_diff(name: str):
137
+ from core.collectors.diff import deployment_diff
138
+ return deployment_diff(name)
139
+
140
+
141
+ # ─── Scaling: HPA, PDB, Capacity, Quota, Drain ───────────────────────────────
142
+
143
+ @router.get("/hpa")
144
+ def get_hpa():
145
+ from core.collectors.scaling import list_hpa
146
+ return {"hpa": list_hpa()}
147
+
148
+ @router.get("/pdb")
149
+ def get_pdb():
150
+ from core.collectors.scaling import list_pdb
151
+ return {"pdb": list_pdb()}
152
+
153
+ @router.get("/capacity")
154
+ def get_capacity():
155
+ from core.collectors.scaling import cluster_capacity
156
+ return cluster_capacity()
157
+
158
+ @router.get("/quota")
159
+ def get_quota():
160
+ from core.collectors.scaling import namespace_quota
161
+ return namespace_quota()
162
+
163
+ @router.get("/drain-check/{node}")
164
+ def get_drain_check(node: str):
165
+ from core.collectors.scaling import drain_check
166
+ return drain_check(node)
167
+
168
+
169
+ # ─── Timeline ────────────────────────────────────────────────────────────────
170
+
171
+ @router.get("/timeline")
172
+ def get_timeline(minutes: int = 60):
173
+ from core.collectors.timeline import build_timeline
174
+ return {"events": build_timeline(minutes=minutes)}
175
+
176
+
177
+ # ─── Ingress / Mesh / Dependencies / DNS ──────────────────────────────────────
178
+
179
+ @router.get("/ingress")
180
+ def get_ingress():
181
+ from core.collectors.services import list_ingresses
182
+ return {"ingresses": list_ingresses()}
183
+
184
+ @router.get("/mesh")
185
+ def get_mesh():
186
+ from core.collectors.services import detect_mesh
187
+ return detect_mesh()
188
+
189
+ @router.get("/deps/{name}")
190
+ def get_deps(name: str):
191
+ from core.collectors.services import service_dependencies
192
+ return service_dependencies(name)
193
+
194
+ @router.get("/dns/{service}")
195
+ def get_dns(service: str):
196
+ from core.collectors.services import dns_debug
197
+ return dns_debug(service)
198
+
199
+
200
+ # ─── Correlate / Playbook ─────────────────────────────────────────────────────
201
+
202
+ @router.get("/correlate")
203
+ def get_correlate(target: Optional[str] = None):
204
+ from core.ai.correlation import correlate
205
+ return {"chains": correlate(target)}
206
+
207
+ @router.get("/playbook/{issue}")
208
+ def get_playbook(issue: str):
209
+ from core.ai.playbooks import get_playbook
210
+ return get_playbook(issue)
211
+
212
+
213
+ # ─── Changelog / Snap ─────────────────────────────────────────────────────────
214
+
215
+ @router.get("/changelog")
216
+ def get_changelog():
217
+ from core.collectors.changes import build_changelog
218
+ return {"changelog": build_changelog()}
219
+
220
+ @router.post("/snap")
221
+ def post_snap():
222
+ from core.collectors.changes import take_state_snapshot
223
+ path = take_state_snapshot()
224
+ return {"path": path}
225
+
226
+ @router.get("/snap-diff")
227
+ def get_snap_diff():
228
+ from core.collectors.changes import get_latest_snapshot, diff_snapshots
229
+ old = get_latest_snapshot()
230
+ return diff_snapshots(old)
231
+
232
+
233
+ # ─── Labels ───────────────────────────────────────────────────────────────────
234
+
235
+ @router.get("/labels/{resource_type}")
236
+ def get_labels(resource_type: str, name: Optional[str] = None):
237
+ from core.collectors.labels import get_labels as fetch_labels
238
+ return {"resources": fetch_labels(resource_type, name)}
239
+
240
+
241
+ # ─── Audit ────────────────────────────────────────────────────────────────────
242
+
243
+ @router.get("/audit")
244
+ def get_audit():
245
+ from core.audit import get_audit_log
246
+ return {"log": get_audit_log()}
247
+
248
+
249
+ # ─── Export ───────────────────────────────────────────────────────────────────
250
+
251
+ @router.get("/export")
252
+ def get_export(format: str = "md"):
253
+ from core.export import export_report
254
+ path = export_report(format=format)
255
+ return {"path": path, "format": format}
256
+
257
+
258
+ # ─── Explain / Generate ───────────────────────────────────────────────────────
259
+
260
+ class ExplainRequest(BaseModel):
261
+ query: str
262
+
263
+ class GenerateRequest(BaseModel):
264
+ kind: str
265
+ name: str
266
+
267
+ @router.post("/explain")
268
+ def post_explain(req: ExplainRequest):
269
+ from core.ai.explain import explain
270
+ result = explain(req.query)
271
+ # Strip Rich markup
272
+ content = re.sub(r'\[/?[^\]]+\]', '', result.get("content", ""))
273
+ return {"title": result.get("title", ""), "content": content}
274
+
275
+ @router.post("/generate")
276
+ def post_generate(req: GenerateRequest):
277
+ from core.ai.generator import generate_manifest
278
+ yaml_output = generate_manifest(req.kind, req.name, context.namespace)
279
+ return {"yaml": yaml_output or ""}