Files
felhom.eu/hub/internal/web/templates/config_form.html
T
admin 17cc67f7cd hub v0.39.0: offsite hardening — F4 credential re-issue, F2 scan retry, F5 save UX
F4: ReissueCredentials — explicit operator recovery for consumed-password
dead-ends; resets the labelled resource's password (exactly-1 guard,
red-proofed), stores a fresh one-time secret, bumps ConfigVersion.
New hetznerapi.ResetBoxPassword for the dedicated path.
F2: host-key scan retry-with-backoff (~60s ladder, red-proofed) — first
save survives fresh-subaccount DNS lag.
F5: config form disables submits + shows an in-flight notice (the re-click
bait that caused live F1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-09 22:39:08 +02:00

177 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Felhom Hub — {{if .IsNew}}Add Customer{{else}}Edit {{.Config.CustomerID}}{{end}}</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
{{template "icon_sprite"}}
<div class="container">
<header>
<h1>Felhom <span>Hub</span></h1>
<nav class="nav-links">
<a href="/" class="nav-link">Dashboard</a>
<a href="/configs" class="nav-link active">Customers</a>
<a href="/apps" class="nav-link">Apps</a>
<a href="/hosts" class="nav-link">Hosts</a>
<a href="/offsite" class="nav-link">Offsite</a>
<a href="/configuration" class="nav-link">Configuration</a>
</nav>
</header>
<a href="{{if .IsNew}}/configs{{else}}/customers/{{.Config.CustomerID}}{{end}}" class="back-link">&larr; Back</a>
<h2>{{if .IsNew}}Add Customer{{else}}Edit: {{.Config.CustomerID}}{{end}}</h2>
{{if .Error}}
<div class="flash flash-error">{{.Error}}</div>
{{end}}
<form method="POST" action="{{if .IsNew}}/configs/new{{else}}/configs/{{.Config.CustomerID}}/edit{{end}}" class="config-form">
{{.CSRFField}}
<div class="card">
<h2>Customer Identity</h2>
<div class="form-grid">
<div class="form-group">
<label for="customer_id">Customer ID *</label>
<input type="text" id="customer_id" name="customer_id"
value="{{.Config.CustomerID}}"
pattern="[a-zA-Z0-9.\-]+"
placeholder="e.g. customer-1"
{{if not .IsNew}}readonly{{end}}
required>
{{if .IsNew}}<span class="form-hint">Letters, numbers, dots, hyphens only</span>{{end}}
</div>
<div class="form-group">
<label for="customer_name">Display Name *</label>
<input type="text" id="customer_name" name="customer_name"
value="{{.Config.CustomerName}}"
placeholder="e.g. Kovács család"
required>
</div>
<div class="form-group">
<label for="domain">Domain *</label>
<input type="text" id="domain" name="domain"
value="{{.Config.Domain}}"
placeholder="e.g. kovacs.felhom.eu"
required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email"
value="{{.Config.Email}}"
placeholder="e.g. kovacs@example.com">
</div>
</div>
</div>
<details class="card" {{if index .Overrides "infrastructure"}}open{{end}}>
<summary><h2 style="display:inline">Infrastructure</h2></summary>
<div class="form-grid" style="margin-top: 1rem;">
<div class="form-group">
<label for="cf_tunnel_token">Cloudflare Tunnel Token</label>
<input type="text" id="cf_tunnel_token" name="cf_tunnel_token"
value="{{with .Overrides}}{{with index . "infrastructure"}}{{with index . "cf_tunnel_token"}}{{.}}{{end}}{{end}}{{end}}"
placeholder="Optional">
</div>
<div class="form-group">
<label for="cf_api_token">Cloudflare API Token</label>
<input type="text" id="cf_api_token" name="cf_api_token"
value="{{with .Overrides}}{{with index . "infrastructure"}}{{with index . "cf_api_token"}}{{.}}{{end}}{{end}}{{end}}"
placeholder="DNS-01 + WAF permissions">
<small class="form-hint">Szükséges jogosultságok: Zone DNS:Edit (ACME), Zone WAF:Edit (geo-korlátozás)</small>
</div>
</div>
</details>
<details class="card" {{if index .Overrides "git"}}open{{end}}>
<summary><h2 style="display:inline">Git Sync</h2></summary>
<div class="form-grid" style="margin-top: 1rem;">
<div class="form-group">
<label for="git_username">Git Username</label>
<input type="text" id="git_username" name="git_username"
value="{{with .Overrides}}{{with index . "git"}}{{with index . "username"}}{{.}}{{end}}{{end}}{{end}}"
placeholder="For private catalog">
</div>
<div class="form-group">
<label for="git_token">Git Token</label>
<input type="text" id="git_token" name="git_token"
value="{{with .Overrides}}{{with index . "git"}}{{with index . "token"}}{{.}}{{end}}{{end}}{{end}}"
placeholder="For private catalog">
</div>
</div>
</details>
<details class="card" {{if index .Overrides "offsite"}}open{{end}}>
<summary><h2 style="display:inline">Offsite backup</h2></summary>
<div class="form-grid" style="margin-top: 1rem;">
<div class="form-group">
<label><input type="checkbox" name="offsite_enabled"
{{with .Overrides}}{{with index . "offsite"}}{{if index . "enabled"}}checked{{end}}{{end}}{{end}}>
Enable offsite (provisions a Hetzner Storage Box on save)</label>
</div>
<div class="form-group">
<label for="offsite_type">Type</label>
<select id="offsite_type" name="offsite_type">
<option value="shared" {{with .Overrides}}{{with index . "offsite"}}{{if eq (index . "type") "shared"}}selected{{end}}{{end}}{{end}}>Shared (sub-account on the pool box)</option>
<option value="dedicated" {{with .Overrides}}{{with index . "offsite"}}{{if eq (index . "type") "dedicated"}}selected{{end}}{{end}}{{end}}>Dedicated (own box)</option>
</select>
</div>
<div class="form-group">
<label for="offsite_quota_gb">Soft-quota GB (shared)</label>
<input type="number" id="offsite_quota_gb" name="offsite_quota_gb"
value="{{with .Overrides}}{{with index . "offsite"}}{{with index . "quota_gb"}}{{.}}{{end}}{{end}}{{end}}" placeholder="e.g. 50">
</div>
<div class="form-group">
<label for="offsite_box_type">Box type (dedicated)</label>
<select id="offsite_box_type" name="offsite_box_type">
<option value="bx11" {{with .Overrides}}{{with index . "offsite"}}{{if eq (index . "box_type") "bx11"}}selected{{end}}{{end}}{{end}}>bx11 (1 TB)</option>
<option value="bx21" {{with .Overrides}}{{with index . "offsite"}}{{if eq (index . "box_type") "bx21"}}selected{{end}}{{end}}{{end}}>bx21 (5 TB)</option>
<option value="bx31" {{with .Overrides}}{{with index . "offsite"}}{{if eq (index . "box_type") "bx31"}}selected{{end}}{{end}}{{end}}>bx31 (10 TB)</option>
</select>
</div>
</div>
{{with .Overrides}}{{with index . "offsite"}}{{if index . "host"}}
<p class="form-hint" style="margin-top:.5rem">Provisioned: {{index . "user"}}@{{index . "host"}}:{{index . "repo_path"}} — the transient password is delivered to the controller once (never shown here).</p>
<!-- F4: explicit operator recovery for a consumed-password dead-end (fresh-guest DR).
Rides the parent form (nested forms are invalid HTML) via formaction; the _csrf field
submits with it. Resets the box credential + stages a fresh one-time password. -->
<button type="submit" class="btn btn-outline" style="margin-top:.5rem"
formaction="/configs/{{$.Config.CustomerID}}/offsite-reissue" formmethod="POST"
onclick="return confirm('Re-issue the offsite credentials?\n\nThe box password is reset and a fresh one-time password is staged for the controller. Guests with a working installed key are unaffected (key-auth-first); a stuck fresh guest picks the new password up on its next config refresh.')">
Re-issue offsite credentials</button>
{{end}}{{end}}{{end}}
</details>
<div style="margin-top: 1.5rem; display: flex; gap: 1rem; align-items: center;">
<button type="submit" class="btn">{{if .IsNew}}Create Configuration{{else}}Save Changes{{end}}</button>
<a href="{{if .IsNew}}/configs{{else}}/customers/{{.Config.CustomerID}}{{end}}" class="btn btn-outline">Cancel</a>
<span id="cfg-inflight" class="form-hint" style="display:none">Saving… offsite provisioning can take up to a minute — please do not click again.</span>
</div>
</form>
<script>
// F5: the offsite save takes ~2560s (create + action-wait + host-key scan). Without feedback
// operators re-click, which used to strand the provision (live finding F1). Disable all submit
// buttons + show the in-flight notice once a submit is underway (deferred a tick so the
// clicked button's formaction still applies).
(function () {
var form = document.querySelector('form.config-form');
if (!form) return;
form.addEventListener('submit', function () {
document.getElementById('cfg-inflight').style.display = 'inline';
var btns = form.querySelectorAll('button[type=submit]');
setTimeout(function () {
for (var i = 0; i < btns.length; i++) btns[i].disabled = true;
}, 0);
});
})();
</script>
<footer>
<p>Felhom Hub {{hubVersion}} — Configuration Management</p>
</footer>
</div>
</body>
</html>