feat: add phase 2 endpoints, session persistence and actions

Co-authored-by: aider (openrouter/thinkingmachines/inkling-small:free) <aider@aider.chat>
This commit is contained in:
Jiří Stachandaider committed 2026-10-09 15:42:31 +02:00
1 parent 18ca7b5885
commit 82ee3aa6c2
4 files changed
+249 -33

No files matched your search

+149 -9
View File
@@ -1,7 +1,31 @@
document.addEventListener('DOMContentLoaded', () => {
const contextSelect = document.getElementById('context');
const namespaceSelect = document.getElementById('namespace');
const podList = document.getElementById('pod-list');
const resourceSelect = document.getElementById('resource-type');
const resourceList = document.getElementById('resource-list');
const resourceTitle = document.getElementById('resource-title');
const searchInput = document.getElementById('search-input');
const searchBtn = document.getElementById('search-btn');
const setDefaultBtn = document.getElementById('set-default');
const actionName = document.getElementById('action-name');
const actionOutput = document.getElementById('action-output');
let currentData = [];
function saveSession() {
sessionStorage.setItem('k8s_context', contextSelect.value);
sessionStorage.setItem('k8s_namespace', namespaceSelect.value);
sessionStorage.setItem('k8s_resource', resourceSelect.value);
}
function loadSession() {
const ctx = sessionStorage.getItem('k8s_context');
const ns = sessionStorage.getItem('k8s_namespace');
const res = sessionStorage.getItem('k8s_resource');
if (ctx) contextSelect.value = ctx;
if (ns) namespaceSelect.value = ns;
if (res) resourceSelect.value = res;
}
async function loadContexts() {
try {
@@ -15,6 +39,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (ctx === data.current) opt.selected = true;
contextSelect.appendChild(opt);
});
loadSession();
loadNamespaces();
} catch (e) {
console.error('Failed to load contexts', e);
}
@@ -22,7 +48,10 @@ document.addEventListener('DOMContentLoaded', () => {
async function loadNamespaces() {
try {
const res = await fetch('http://localhost:8000/namespaces');
const ctx = contextSelect.value || null;
const url = new URL('http://localhost:8000/namespaces');
if (ctx) url.searchParams.set('context', ctx);
const res = await fetch(url);
const data = await res.json();
namespaceSelect.innerHTML = '';
data.namespaces.forEach(ns => {
@@ -31,24 +60,135 @@ document.addEventListener('DOMContentLoaded', () => {
opt.textContent = ns;
namespaceSelect.appendChild(opt);
});
loadSession();
loadResources();
} catch (e) {
console.error('Failed to load namespaces', e);
}
}
async function loadPods() {
async function loadResources() {
const resType = resourceSelect.value;
const ns = namespaceSelect.value || 'default';
const ctx = contextSelect.value || null;
resourceTitle.textContent = resType.charAt(0).toUpperCase() + resType.slice(1);
resourceList.innerHTML = '<li>Loading...</li>';
try {
const res = await fetch(`http://localhost:8000/pods?namespace=${encodeURIComponent(ns)}`);
let url;
if (resType === 'pods') {
url = `http://localhost:8000/pods?namespace=${encodeURIComponent(ns)}${ctx ? '&context=' + encodeURIComponent(ctx) : ''}`;
} else if (resType === 'nodes') {
url = `http://localhost:8000/nodes${ctx ? '?context=' + encodeURIComponent(ctx) : ''}`;
} else if (resType === 'pvcs') {
url = `http://localhost:8000/pvcs?namespace=${encodeURIComponent(ns)}${ctx ? '&context=' + encodeURIComponent(ctx) : ''}`;
} else {
url = `http://localhost:8000/pods?namespace=${encodeURIComponent(ns)}`;
}
const res = await fetch(url);
const data = await res.json();
podList.innerHTML = '<li>Pods loaded (raw output available)</li>';
currentData = [];
try {
const obj = JSON.parse(data.raw || '{}');
if (resType === 'pods' || resType === 'pvcs') {
const items = obj.items || [];
currentData = items.map(i => ({
name: i.metadata?.name || 'unknown',
namespace: i.metadata?.namespace || ns
}));
} else if (resType === 'nodes') {
const items = obj.items || [];
currentData = items.map(i => ({
name: i.metadata?.name || 'unknown',
namespace: '-'
}));
}
} catch (e) {
currentData = [];
}
renderList();
} catch (e) {
console.error('Failed to load pods', e);
console.error('Failed to load resources', e);
resourceList.innerHTML = '<li>Error loading resources</li>';
}
}
function renderList() {
const filter = searchInput.value.toLowerCase();
resourceList.innerHTML = '';
const filtered = currentData.filter(item => item.name.toLowerCase().includes(filter));
if (filtered.length === 0) {
resourceList.innerHTML = '<li>No results</li>';
return;
}
filtered.forEach(item => {
const li = document.createElement('li');
li.innerHTML = `<strong>${item.name}</strong> <span style="color:#666">(${item.namespace})</span>`;
resourceList.appendChild(li);
});
}
contextSelect.addEventListener('change', () => { saveSession(); loadNamespaces(); });
namespaceSelect.addEventListener('change', () => { saveSession(); loadResources(); });
resourceSelect.addEventListener('change', () => { saveSession(); loadResources(); });
searchBtn.addEventListener('click', renderList);
searchInput.addEventListener('input', renderList);
setDefaultBtn.addEventListener('click', () => { saveSession(); alert('Defaults saved for session'); });
document.getElementById('btn-describe').addEventListener('click', async () => {
const name = actionName.value.trim();
if (!name) return;
const resType = resourceSelect.value === 'pvcs' ? 'pvc' : (resourceSelect.value === 'nodes' ? 'node' : 'pod');
const ns = namespaceSelect.value || 'default';
const ctx = contextSelect.value || null;
try {
const url = new URL('http://localhost:8000/describe');
url.searchParams.set('resource', resType);
url.searchParams.set('name', name);
url.searchParams.set('namespace', ns);
if (ctx) url.searchParams.set('context', ctx);
const res = await fetch(url);
const data = await res.json();
actionOutput.textContent = data.output || data.error || JSON.stringify(data);
} catch (e) {
actionOutput.textContent = 'Error: ' + e;
}
});
document.getElementById('btn-logs').addEventListener('click', async () => {
const name = actionName.value.trim();
if (!name) return;
const ns = namespaceSelect.value || 'default';
const ctx = contextSelect.value || null;
try {
const url = new URL('http://localhost:8000/logs');
url.searchParams.set('pod', name);
url.searchParams.set('namespace', ns);
if (ctx) url.searchParams.set('context', ctx);
const res = await fetch(url);
const data = await res.json();
actionOutput.textContent = data.output || data.error || JSON.stringify(data);
} catch (e) {
actionOutput.textContent = 'Error: ' + e;
}
});
document.getElementById('btn-delete').addEventListener('click', async () => {
const name = actionName.value.trim();
if (!name || !confirm('Delete ' + name + '?')) return;
const resType = resourceSelect.value === 'pvcs' ? 'pvc' : (resourceSelect.value === 'nodes' ? 'node' : 'pod');
const ns = namespaceSelect.value || 'default';
const ctx = contextSelect.value || null;
try {
const url = `http://localhost:8000/delete?resource=${encodeURIComponent(resType)}&name=${encodeURIComponent(name)}&namespace=${encodeURIComponent(ns)}${ctx ? '&context=' + encodeURIComponent(ctx) : ''}`;
const res = await fetch(url, { method: 'POST' });
const data = await res.json();
actionOutput.textContent = data.output || data.error || JSON.stringify(data);
loadResources();
} catch (e) {
actionOutput.textContent = 'Error: ' + e;
}
});
loadContexts();
loadNamespaces();
namespaceSelect.addEventListener('change', loadPods);
loadPods();
});
+28 -2
View File
@@ -17,11 +17,37 @@
<label for="namespace">Namespace:</label>
<select id="namespace"><option>default</option></select>
<label for="resource-type">Resource:</label>
<select id="resource-type">
<option value="pods">Pods</option>
<option value="nodes">Nodes</option>
<option value="pvcs">PVCs</option>
</select>
<button id="set-default">Set as default</button>
</section>
<section id="search">
<input type="text" id="search-input" placeholder="Search by name..." />
<button id="search-btn">Search / Refresh</button>
</section>
<section id="resources">
<h2>Pods</h2>
<ul id="pod-list"><li>Loading...</li></ul>
<h2 id="resource-title">Pods</h2>
<ul id="resource-list"><li>Loading...</li></ul>
</section>
<section id="actions">
<h2>Actions</h2>
<div>
<label>Resource name:</label>
<input type="text" id="action-name" placeholder="name" />
<button id="btn-describe">Describe</button>
<button id="btn-logs">Logs</button>
<button id="btn-delete" class="danger">Delete</button>
</div>
<pre id="action-output"></pre>
</section>
</main>
<script src="app.js"></script>