Add docker-ip-addr-manager initial app

This commit is contained in:
Joachim Friberg
2026-03-18 21:43:59 +01:00
parent 2fddde0129
commit 69011271fc
19 changed files with 1920 additions and 0 deletions
@@ -0,0 +1,126 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Docker IP Addr Manager</title>
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
<main id="app" class="app-shell" v-cloak>
<header class="topbar">
<div>
<h1>Docker IP Addr Manager</h1>
<p>Manage host IP addresses used for ZimaOS port bindings.</p>
</div>
<button type="button" class="btn btn-secondary" @click="refreshEntries" :disabled="loading">
{{ loading ? 'Refreshing...' : 'Refresh' }}
</button>
</header>
<section class="panel">
<h2>Add IP Entry</h2>
<form class="entry-form" @submit.prevent="createEntry">
<label>
Name
<input v-model.trim="form.name" type="text" required maxlength="96" />
</label>
<label>
IP Address
<input v-model.trim="form.ip" type="text" placeholder="10.0.4.2" required />
</label>
<label>
CIDR
<input v-model.number="form.cidr" type="number" min="0" max="32" required />
</label>
<label>
Device
<select v-model="form.device" required>
<option disabled value="">Choose device</option>
<option v-for="iface in interfaces" :key="iface" :value="iface">{{ iface }}</option>
</select>
</label>
<button class="btn btn-primary" type="submit" :disabled="saving">Add</button>
</form>
</section>
<section class="panel">
<div class="status" v-if="errorMessage">{{ errorMessage }}</div>
<div class="status warning" v-if="!usageKnown">
Docker usage status is unknown. Disable/Delete operations are fail-closed until refresh succeeds.
<span v-if="usageError">Error: {{ usageError }}</span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th @click="setSort('name')">Name <span>{{ sortIndicator('name') }}</span></th>
<th @click="setSort('ip')">IP Address <span>{{ sortIndicator('ip') }}</span></th>
<th @click="setSort('cidr')">CIDR <span>{{ sortIndicator('cidr') }}</span></th>
<th @click="setSort('used')">Used <span>{{ sortIndicator('used') }}</span></th>
<th @click="setSort('containers')">Container(s) <span>{{ sortIndicator('containers') }}</span></th>
<th @click="setSort('device')">Device <span>{{ sortIndicator('device') }}</span></th>
<th @click="setSort('enabled')">Enabled <span>{{ sortIndicator('enabled') }}</span></th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="entry in sortedEntries" :key="entry.id">
<td><input v-model.trim="entry.draft.name" type="text" maxlength="96" :disabled="entry.enabled" /></td>
<td><input v-model.trim="entry.draft.ip" type="text" :disabled="entry.enabled" /></td>
<td><input v-model.number="entry.draft.cidr" type="number" min="0" max="32" :disabled="entry.enabled" /></td>
<td class="status-cell">
<span v-if="!entry.usage_known" class="chip unknown">Unknown</span>
<span v-else-if="entry.used" class="chip used">Yes</span>
<span v-else class="chip free">No</span>
</td>
<td class="containers">{{ containerLabel(entry) }}</td>
<td>
<select v-model="entry.draft.device" :disabled="entry.enabled">
<option v-for="iface in interfaces" :key="`${entry.id}-${iface}`" :value="iface">{{ iface }}</option>
</select>
</td>
<td class="toggle-cell">
<button
type="button"
class="btn"
:class="entry.enabled ? 'btn-danger' : 'btn-primary'"
@click="toggleEntry(entry)"
:disabled="isBusy(entry.id)"
>
{{ entry.enabled ? 'Disable' : 'Enable' }}
</button>
</td>
<td class="actions">
<button
type="button"
class="btn btn-secondary"
@click="saveEntry(entry)"
:disabled="entry.enabled || isBusy(entry.id)"
>Save</button>
<button
type="button"
class="btn btn-danger"
@click="deleteEntry(entry)"
:disabled="entry.enabled || isBusy(entry.id)"
>Delete</button>
</td>
</tr>
<tr v-if="sortedEntries.length === 0">
<td colspan="8" class="empty">No entries configured.</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="/static/app.js"></script>
</body>
</html>