Files
vacuum-wall/webui/templates/dhcp.html
T
mteehan 37039351be fix htmx refactor route mismatches and remaining TODO items
- wireguard: POST /peers with JSON encoding (was /add-peer)
- rules: delete by rule_id in URL path (was JSON body); pass rule objects with id from server; add hx-disable to initial render
- nat: port forward delete uses URL path params to match blueprint
- nat: masquerade toggle uses native hx-post/hx-vals (was inline fetch)
- app.js renderers updated to use URL path deletes for rules and forwards
- remove TODO.md
2026-05-17 01:17:12 +00:00

215 lines
9.7 KiB
HTML

{% extends "base.html" %}
{% block title %}DHCP & DNS - Vacuum Wall{% endblock %}
{% block content %}
<div class="page-header">
<div>
<h1>DHCP &amp; DNS</h1>
<div class="subtitle">Dnsmasq configuration and lease management</div>
</div>
</div>
<!-- DHCP Ranges -->
<div class="section-title">DHCP Ranges</div>
<div class="card mb-4">
<form hx-post="/api/dhcp/ranges" hx-swap="none" hx-on::after-request="if(evt.detail.successful){ refreshTable('/api/dhcp/config', document.getElementById('range-rows'), renderRanges); showSuccessToast('DHCP range added'); }">
<div class="inline-form">
<div class="form-group">
<label for="range-interface">Interface</label>
<select id="range-interface" name="interface">
<option value="">— Global —</option>
{% for iface in (interfaces or []) %}
<option value="{{ iface.get('name', '') }}">{{ iface.get('name', '') }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="range-start">Start IP</label>
<input type="text" id="range-start" name="start" placeholder="192.168.1.100" required style="width:160px;">
</div>
<div class="form-group">
<label for="range-end">End IP</label>
<input type="text" id="range-end" name="end" placeholder="192.168.1.200" required style="width:160px;">
</div>
<div class="form-group">
<label for="range-lease">Lease Time</label>
<input type="text" id="range-lease" name="lease_time" placeholder="1h" value="{{ (config or {}).get('dhcp_lease_time', '1h') }}" style="width:80px;">
</div>
<button type="submit" class="btn btn-primary">Add Range</button>
</div>
</form>
<div class="mt-4">
<table>
<thead>
<tr>
<th>Interface</th>
<th>Start</th>
<th>End</th>
<th>Lease Time</th>
<th style="width:80px;">Action</th>
</tr>
</thead>
<tbody id="range-rows">
{% for rng in ((config or {}).get('dhcp_ranges', []) or []) %}
<tr>
<td>{{ rng.get('interface', '(global)') }}</td>
<td>{{ rng.get('start', '') }}</td>
<td>{{ rng.get('end', '') }}</td>
<td>{{ rng.get('lease_time', '1h') }}</td>
<td>
<form hx-delete="/api/dhcp/ranges" hx-encoding="json" hx-vals='{"interface": "{{ rng.get("interface", "") }}", "start": "{{ rng.get("start", "") }}", "end": "{{ rng.get("end", "") }}" }' hx-swap="none" hx-on::after-request="if(evt.detail.successful){ refreshTable('/api/dhcp/config', document.getElementById('range-rows'), renderRanges); showSuccessToast('Range removed'); }">
<button type="submit" class="btn btn-sm btn-danger" hx-confirm="Remove DHCP range {{ rng.get('start', '') }} - {{ rng.get('end', '') }}?">Remove</button>
</form>
</td>
</tr>
{% endfor %}
{% if not ((config or {}).get('dhcp_ranges', []) or []) %}
<tr>
<td colspan="5" class="text-muted text-sm">No DHCP ranges configured</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<!-- Static Leases -->
<div class="section-title">Static Leases</div>
<div class="card mb-4">
<form hx-post="/api/dhcp/static-lease" hx-swap="none" hx-on::after-request="if(evt.detail.successful){ refreshTable('/api/dhcp/config', document.getElementById('lease-rows'), renderStaticLeases); showSuccessToast('Static lease added'); }">
<div class="inline-form">
<div class="form-group">
<label for="lease-mac">MAC Address</label>
<input type="text" id="lease-mac" name="mac" placeholder="aa:bb:cc:dd:ee:ff" required style="width:180px;">
</div>
<div class="form-group">
<label for="lease-ip">IP Address</label>
<input type="text" id="lease-ip" name="ip" placeholder="192.168.1.50" required style="width:160px;">
</div>
<div class="form-group">
<label for="lease-host">Hostname</label>
<input type="text" id="lease-host" name="hostname" placeholder="myhost" style="width:140px;">
</div>
<button type="submit" class="btn btn-primary">Add Lease</button>
</div>
</form>
<div class="mt-4">
<table>
<thead>
<tr>
<th>MAC</th>
<th>IP</th>
<th>Hostname</th>
<th style="width:80px;">Action</th>
</tr>
</thead>
<tbody id="lease-rows">
{% for lease in ((config or {}).get('static_leases', []) or []) %}
<tr>
<td>{{ lease.get('mac', '') }}</td>
<td>{{ lease.get('ip', '') }}</td>
<td>{{ lease.get('hostname', '-') }}</td>
<td>
<form hx-delete="/api/dhcp/static-lease/{{ lease.get('mac', '') }}" hx-swap="none" hx-on::after-request="if(evt.detail.successful){ refreshTable('/api/dhcp/config', document.getElementById('lease-rows'), renderStaticLeases); showSuccessToast('Lease removed'); }">
<button type="submit" class="btn btn-sm btn-danger" hx-confirm="Remove lease {{ lease.get('mac', '') }}?">Remove</button>
</form>
</td>
</tr>
{% endfor %}
{% if not ((config or {}).get('static_leases', []) or []) %}
<tr>
<td colspan="4" class="text-muted text-sm">No static leases configured</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<!-- Custom DNS Records -->
<div class="section-title">Custom DNS Records</div>
<div class="card mb-4">
<form hx-post="/api/dhcp/dns-record" hx-swap="none" hx-on::after-request="if(evt.detail.successful){ refreshTable('/api/dhcp/config', document.getElementById('dns-rows'), renderDnsRecords); showSuccessToast('DNS record added'); }">
<div class="inline-form">
<div class="form-group">
<label for="dns-ip">IP Address</label>
<input type="text" id="dns-ip" name="address" placeholder="192.168.1.10" required style="width:160px;">
</div>
<div class="form-group">
<label for="dns-hostname">Hostname / Domain</label>
<input type="text" id="dns-hostname" name="name" placeholder="host.local" required style="width:200px;">
</div>
<button type="submit" class="btn btn-primary">Add Record</button>
</div>
</form>
<div class="mt-4">
<table>
<thead>
<tr>
<th>Name</th>
<th>Address</th>
<th style="width:80px;">Action</th>
</tr>
</thead>
<tbody id="dns-rows">
{% for rec in ((config or {}).get('dns_records', []) or []) %}
<tr>
<td><strong>{{ rec.get('name', 'unnamed') }}</strong></td>
<td class="text-sm">{{ rec.get('address', '-') }}</td>
<td>
<form hx-delete="/api/dhcp/dns-record/{{ rec.get('name', '') }}" hx-swap="none" hx-on::after-request="if(evt.detail.successful){ refreshTable('/api/dhcp/config', document.getElementById('dns-rows'), renderDnsRecords); showSuccessToast('Record removed'); }">
<button type="submit" class="btn btn-sm btn-danger" hx-confirm="Remove DNS record {{ rec.get('name', '') }}?">Remove</button>
</form>
</td>
</tr>
{% endfor %}
{% if not ((config or {}).get('dns_records', []) or []) %}
<tr>
<td colspan="3" class="text-muted text-sm">No custom DNS records</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<!-- Current DHCP Leases -->
<div class="section-title">Current DHCP Leases</div>
<div class="card">
<table>
<thead>
<tr>
<th>Expires</th>
<th>MAC</th>
<th>IP</th>
<th>Hostname</th>
<th>Client ID</th>
</tr>
</thead>
<tbody>
{% for lease in (leases or []) %}
<tr>
<td>{{ lease.get('expires', 'N/A') }}</td>
<td>{{ lease.get('mac', 'N/A') }}</td>
<td>{{ lease.get('ip', 'N/A') }}</td>
<td>{{ lease.get('hostname', '*') or '*' }}</td>
<td class="text-muted text-sm">{{ lease.get('client_id', 'N/A') }}</td>
</tr>
{% endfor %}
{% if not (leases or []) %}
<tr>
<td colspan="5" class="text-muted text-sm">No active DHCP leases</td>
</tr>
{% endif %}
</tbody>
</table>
<div class="mt-2 text-right">
<button class="btn btn-sm btn-outline" hx-post="/api/dhcp/apply" hx-swap="none" hx-on::after-request="if(evt.detail.successful){ showSuccessToast('Dnsmasq configuration reloaded'); }">Apply &amp; Restart Dnsmasq</button>
</div>
</div>
{% endblock %}