d1ab717c0f
- Fix WireGuard private key leak in API responses and config updates - Update systemd service to serve from repo root with adjusted sandbox - Add CLI flags, idempotency, and dev mode to install.sh - Extract common utilities to lib/common.py and webui/api/common.py - Migrate frontend to htmx for simpler, more maintainable UI - Update docs to reflect current architecture and deployment model - Vendor htmx dependencies per project requirements
215 lines
9.8 KiB
HTML
215 lines
9.8 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}DHCP & DNS - Vacuum Wall{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-header">
|
|
<div>
|
|
<h1>DHCP & 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-encoding="json" 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-encoding="json" 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-encoding="json" 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 & Restart Dnsmasq</button>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|