const { createApp } = Vue; function mapEntry(entry) { return { ...entry, draft: { name: entry.name, ip: entry.ip, cidr: entry.cidr, device: entry.device, }, }; } createApp({ data() { return { interfaces: [], entries: [], usageKnown: true, usageError: "", loading: false, saving: false, busyById: {}, errorMessage: "", form: { name: "", ip: "", cidr: 16, device: "", }, sort: { key: "name", direction: "asc", }, }; }, computed: { sortedEntries() { const list = [...this.entries]; const key = this.sort.key; const multiplier = this.sort.direction === "asc" ? 1 : -1; const extractors = { name: (entry) => entry.name.toLowerCase(), ip: (entry) => entry.ip, cidr: (entry) => entry.cidr, used: (entry) => (entry.usage_known ? (entry.used ? 1 : 0) : -1), containers: (entry) => entry.containers.join(",").toLowerCase(), device: (entry) => entry.device.toLowerCase(), enabled: (entry) => (entry.enabled ? 1 : 0), }; const extract = extractors[key] || extractors.name; list.sort((a, b) => { const left = extract(a); const right = extract(b); if (left < right) { return -1 * multiplier; } if (left > right) { return 1 * multiplier; } return a.name.localeCompare(b.name) * multiplier; }); return list; }, }, methods: { async api(path, options = {}) { const response = await fetch(path, { headers: { "Content-Type": "application/json", }, ...options, }); let payload = {}; try { payload = await response.json(); } catch (err) { payload = {}; } if (!response.ok) { const message = payload.detail || `Request failed: ${response.status}`; throw new Error(message); } return payload; }, async loadInterfaces() { const data = await this.api("/api/interfaces"); this.interfaces = data.items || []; if (!this.form.device && this.interfaces.length > 0) { this.form.device = this.interfaces[0]; } }, async refreshEntries() { this.loading = true; this.errorMessage = ""; try { const data = await this.api("/api/refresh", { method: "POST" }); this.entries = (data.items || []).map(mapEntry); this.usageKnown = Boolean(data.usage_known); this.usageError = data.usage_error || ""; } catch (err) { this.errorMessage = err.message; } finally { this.loading = false; } }, async createEntry() { this.saving = true; this.errorMessage = ""; try { await this.api("/api/entries", { method: "POST", body: JSON.stringify({ name: this.form.name, ip: this.form.ip, cidr: this.form.cidr, device: this.form.device, }), }); this.form.name = ""; this.form.ip = ""; await this.refreshEntries(); } catch (err) { this.errorMessage = err.message; } finally { this.saving = false; } }, async saveEntry(entry) { this.errorMessage = ""; this.busyById = { ...this.busyById, [entry.id]: true }; try { await this.api(`/api/entries/${entry.id}`, { method: "PUT", body: JSON.stringify({ name: entry.draft.name, ip: entry.draft.ip, cidr: entry.draft.cidr, device: entry.draft.device, }), }); await this.refreshEntries(); } catch (err) { this.errorMessage = err.message; } finally { this.busyById = { ...this.busyById, [entry.id]: false }; } }, async toggleEntry(entry) { this.errorMessage = ""; this.busyById = { ...this.busyById, [entry.id]: true }; try { const action = entry.enabled ? "disable" : "enable"; await this.api(`/api/entries/${entry.id}/${action}`, { method: "POST" }); await this.refreshEntries(); } catch (err) { this.errorMessage = err.message; } finally { this.busyById = { ...this.busyById, [entry.id]: false }; } }, async deleteEntry(entry) { this.errorMessage = ""; this.busyById = { ...this.busyById, [entry.id]: true }; try { await this.api(`/api/entries/${entry.id}`, { method: "DELETE" }); await this.refreshEntries(); } catch (err) { this.errorMessage = err.message; } finally { this.busyById = { ...this.busyById, [entry.id]: false }; } }, containerLabel(entry) { if (!entry.containers || entry.containers.length === 0) { return "-"; } if (entry.containers.length === 1) { return entry.containers[0]; } return `${entry.containers.join(", ")} (${entry.containers.length})`; }, setSort(key) { if (this.sort.key === key) { this.sort.direction = this.sort.direction === "asc" ? "desc" : "asc"; return; } this.sort.key = key; this.sort.direction = "asc"; }, sortIndicator(key) { if (this.sort.key !== key) { return ""; } return this.sort.direction === "asc" ? "▲" : "▼"; }, isBusy(entryId) { return Boolean(this.busyById[entryId]); }, }, async mounted() { try { await this.loadInterfaces(); await this.refreshEntries(); } catch (err) { this.errorMessage = err.message; } }, }).mount("#app");