From 82a083900450dcced28f213eae50acd0a710c21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Stach?= Date: Fri, 9 Oct 2026 15:23:11 +0200 Subject: [PATCH] feat: add phase 1 fastapi backend and frontend Co-authored-by: aider (openrouter/thinkingmachines/inkling-small:free) --- .gitignore | 10 +++++++- backend/main.py | 52 ++++++++++++++++++++++++++++++++++++++ backend/requirements.txt | 3 +++ frontend/app.js | 54 ++++++++++++++++++++++++++++++++++++++++ frontend/index.html | 29 +++++++++++++++++++++ frontend/style.css | 42 +++++++++++++++++++++++++++++++ 6 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 backend/main.py create mode 100644 backend/requirements.txt create mode 100644 frontend/app.js create mode 100644 frontend/index.html create mode 100644 frontend/style.css diff --git a/.gitignore b/.gitignore index b0ac3ed..259c3c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,9 @@ -.aider* +__pycache__/ +*.pyc +*.pyo +.env +.venv/ +venv/ +node_modules/ +dist/ +*.egg-info/ diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..8c6e9bd --- /dev/null +++ b/backend/main.py @@ -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} diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..c6dfa17 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,3 @@ +fastapi +uvicorn[standard] +python-multipart diff --git a/frontend/app.js b/frontend/app.js new file mode 100644 index 0000000..d6a1a31 --- /dev/null +++ b/frontend/app.js @@ -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 = '
  • Pods loaded (raw output available)
  • '; + } catch (e) { + console.error('Failed to load pods', e); + } + } + + loadContexts(); + loadNamespaces(); + namespaceSelect.addEventListener('change', loadPods); + loadPods(); +}); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..4fb8701 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,29 @@ + + + + + + Kubectl Web GUI + + + +
    +

    Kubectl Web GUI

    +
    +
    +
    + + + + + +
    + +
    +

    Pods

    +
    • Loading...
    +
    +
    + + + diff --git a/frontend/style.css b/frontend/style.css new file mode 100644 index 0000000..edc1686 --- /dev/null +++ b/frontend/style.css @@ -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; +}