feat: add phase 1 fastapi backend and frontend

Co-authored-by: aider (openrouter/thinkingmachines/inkling-small:free) <aider@aider.chat>
This commit is contained in:
Jiří Stachandaider committed 2026-10-09 15:23:11 +02:00
1 parent e67d1f3316
commit 82a0839004
6 files changed
+189 -1

No files matched your search

+9 -1
View File
@@ -1 +1,9 @@
.aider*
__pycache__/
*.pyc
*.pyo
.env
.venv/
venv/
node_modules/
dist/
*.egg-info/
+52
View File
@@ -0,0 +1,52 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import subprocess
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
def root():
return {"message": "Kubectl Web GUI backend"}
@app.get("/contexts")
def get_contexts():
result = subprocess.run(
["kubectl", "config", "get-contexts", "-o", "name"],
capture_output=True, text=True
)
contexts = [line for line in result.stdout.splitlines() if line]
current = ""
try:
cur = subprocess.run(
["kubectl", "config", "current-context"],
capture_output=True, text=True
)
current = cur.stdout.strip()
except Exception:
pass
return {"current": current, "contexts": contexts}
@app.get("/namespaces")
def get_namespaces():
result = subprocess.run(
["kubectl", "get", "namespaces", "-o", "jsonpath={range .items[*]}{.metadata.name}{\"\\n\"}{end}"],
capture_output=True, text=True
)
namespaces = [line for line in result.stdout.splitlines() if line]
return {"namespaces": namespaces}
@app.get("/pods")
def get_pods(namespace: str = "default"):
result = subprocess.run(
["kubectl", "get", "pods", "-n", namespace, "-o", "json"],
capture_output=True, text=True
)
return {"raw": result.stdout, "namespace": namespace}
+3
View File
@@ -0,0 +1,3 @@
fastapi
uvicorn[standard]
python-multipart
+54
View File
@@ -0,0 +1,54 @@
document.addEventListener('DOMContentLoaded', () => {
const contextSelect = document.getElementById('context');
const namespaceSelect = document.getElementById('namespace');
const podList = document.getElementById('pod-list');
async function loadContexts() {
try {
const res = await fetch('http://localhost:8000/contexts');
const data = await res.json();
contextSelect.innerHTML = '';
data.contexts.forEach(ctx => {
const opt = document.createElement('option');
opt.value = ctx;
opt.textContent = ctx;
if (ctx === data.current) opt.selected = true;
contextSelect.appendChild(opt);
});
} catch (e) {
console.error('Failed to load contexts', e);
}
}
async function loadNamespaces() {
try {
const res = await fetch('http://localhost:8000/namespaces');
const data = await res.json();
namespaceSelect.innerHTML = '';
data.namespaces.forEach(ns => {
const opt = document.createElement('option');
opt.value = ns;
opt.textContent = ns;
namespaceSelect.appendChild(opt);
});
} catch (e) {
console.error('Failed to load namespaces', e);
}
}
async function loadPods() {
const ns = namespaceSelect.value || 'default';
try {
const res = await fetch(`http://localhost:8000/pods?namespace=${encodeURIComponent(ns)}`);
const data = await res.json();
podList.innerHTML = '<li>Pods loaded (raw output available)</li>';
} catch (e) {
console.error('Failed to load pods', e);
}
}
loadContexts();
loadNamespaces();
namespaceSelect.addEventListener('change', loadPods);
loadPods();
});
+29
View File
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kubectl Web GUI</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Kubectl Web GUI</h1>
</header>
<main>
<section id="selectors">
<label for="context">Context:</label>
<select id="context"><option>Loading...</option></select>
<label for="namespace">Namespace:</label>
<select id="namespace"><option>default</option></select>
</section>
<section id="resources">
<h2>Pods</h2>
<ul id="pod-list"><li>Loading...</li></ul>
</section>
</main>
<script src="app.js"></script>
</body>
</html>
+42
View File
@@ -0,0 +1,42 @@
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 2rem;
background: #f5f5f5;
color: #222;
}
header h1 {
margin-bottom: 1rem;
}
#selectors {
background: #fff;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
display: flex;
gap: 1rem;
align-items: center;
}
#selectors label {
font-weight: 600;
}
select {
padding: 0.25rem 0.5rem;
border-radius: 4px;
border: 1px solid #ccc;
}
#resources {
background: #fff;
padding: 1rem;
border-radius: 8px;
}
#pod-list {
list-style: none;
padding: 0;
}