Files
vacuum-wall/webui/templates/certs.html
T
mteehan 7beba44b4b feat: pre-computed state store and async ACME issuance (fixes timeout mismatch)
- Add lib/state.py: in-memory state store with subsystem collectors
  (firewall, dnsmasq, nginx, acme, wireguard)
- Refactor all handlers: read from state on GET, call refresh_state()
  after mutations instead of invoking subprocesses per request
- daemon/server.py: add refresh_state(), /status/all, /status/refresh;
  populate state at startup
- webui/api/certs.py: async step-by-step ACME issuance (validate,
  issue with request_id, poll status) replacing blocking endpoint
- webui/server.py: render pages from state instead of direct lib calls
- Update templates, JS for async cert issuance with polling UI
- Update tests for state-based mocking; add test_state.py
- Fix SIM105 lint issue (contextlib.suppress)
- Add TODO.md with certificate issuance issue tracking

Resolves: WebUI 30s timeout freeze during cert issuance (Problem 1)
2026-05-30 05:45:40 +00:00

95 lines
4.2 KiB
HTML

{% extends "base.html" %}
{% block title %}Certificates - Vacuum Wall{% endblock %}
{% block content %}
<div class="page-header">
<div>
<h1>Certificates</h1>
<div class="subtitle">SSL/TLS certificate management</div>
</div>
<button class="btn btn-primary" onclick="resetIssueWizard(); openModal('issue-cert-modal')">+ Issue New Certificate</button>
</div>
<div class="card">
<table>
<thead>
<tr>
<th>Domain</th>
<th>Issuer</th>
<th>Expiry Date</th>
<th>Days Left</th>
<th style="width:120px;">Actions</th>
</tr>
</thead>
<tbody id="cert-rows">
{% for cert in (certs or []) %}
<tr>
<td><strong>{{ cert.get('domain', 'unknown') }}</strong></td>
<td class="text-sm">{{ cert.get('issuer', '-') }}</td>
<td>{{ cert.get('expiry', 'N/A') }}</td>
<td>
{% set days = cert.get('days_remaining') %}
{% if cert.get('expired') or (days is not none and days <= 0) %}
<span class="badge badge-danger">Expired{% if days %} ({{ days }}d ago){% endif %}</span>
{% elif days is not none and days <= 30 %}
<span class="badge badge-warning">{{ days }} days</span>
{% else %}
<span class="badge badge-success">{{ days }} days</span>
{% endif %}
</td>
<td>
<form hx-post="/api/certs/{{ cert.get('domain', '') }}/renew" hx-swap="none" hx-on::after-request="if(evt.detail.successful){ refreshTable('/api/certs/list', document.getElementById('cert-rows'), renderCerts); showSuccessToast('Renewal started for {{ cert.domain }}'); }">
<button type="submit" class="btn btn-sm btn-outline">Renew</button>
</form>
</td>
</tr>
{% endfor %}
{% if not (certs or []) %}
<tr>
<td colspan="5" class="text-muted text-sm">No certificates found. Issue a certificate to get started.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<!-- Issue Certificate Modal — Phase 1: Validate -->
<div class="modal-overlay" id="issue-cert-modal" onclick="if(event.target===this) closeIssueWizard()">
<div class="modal" style="min-width:480px;">
<h2>Issue New Certificate</h2>
<!-- Phase 1: Input + Pre-flight Checks -->
<div id="cert-wizard-input">
<div class="form-group">
<label for="cert-domain">Domain</label>
<input type="text" id="cert-domain" placeholder="example.com" required>
</div>
<div class="form-group">
<label for="cert-email">Contact Email</label>
<input type="email" id="cert-email" placeholder="admin@example.com" value="{{ (email or '') | e }}">
</div>
<!-- Pre-flight validation results (shown after Check) -->
<div id="cert-check-results" style="display:none;">
<div class="section-title" style="margin-top:16px;">Pre-flight Checks</div>
<div id="cert-checks-list"></div>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-outline" onclick="closeIssueWizard()">Cancel</button>
<button type="button" id="cert-check-btn" class="btn btn-primary" onclick="validateCertIssue()">Check</button>
<button type="button" id="cert-issue-btn" class="btn btn-primary" style="display:none;" onclick="startCertIssue()">Issue</button>
</div>
</div>
<!-- Phase 2: Step progress -->
<div id="cert-wizard-progress" style="display:none;">
<div id="cert-steps-list"></div>
<div class="modal-actions">
<button type="button" class="btn btn-outline" id="cert-close-progress" style="display:none;" onclick="closeIssueWizard(); refreshTable('/api/certs/list', document.getElementById('cert-rows'), renderCerts);">Done</button>
</div>
</div>
</div>
</div>
{% endblock %}