hub: F-16 — inline two-step confirm partial replaces every native confirm() (offsite/PBS re-issue, freeze, telemetry reset, dismiss-all, regen-password, claim-resend, block/delete, geo-disable); hub_confirm_gate.py enforces zero

Claude-Session: https://claude.ai/code/session_01GzammAMzsJTgpQHqxwM2bC
This commit is contained in:
2026-07-13 08:19:54 +02:00
parent bad9203daa
commit 5d66c62bd6
7 changed files with 93 additions and 21 deletions
+4 -4
View File
@@ -6,6 +6,7 @@
<title>{{.AppName}} — Felhom Hub</title>
<link rel="stylesheet" href="/style.css?v={{hubVersion}}">
<script src="/static/chart.min.js"></script>
{{template "inline_confirm_js"}}
</head>
<body>
{{template "icon_sprite"}}
@@ -45,10 +46,9 @@
<section class="card">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<h2 style="margin: 0;">{{if .Summary}}{{if .Summary.DisplayName}}{{.Summary.DisplayName}}{{else}}{{.AppName}}{{end}}{{else}}{{.AppName}}{{end}}</h2>
<form method="POST" action="/apps/{{.AppName}}/reset-telemetry{{if .Period}}?period={{.Period}}{{end}}"
onsubmit="return confirm('Are you sure you want to delete all telemetry data for {{.AppName}}? This cannot be undone.');">
<form method="POST" action="/apps/{{.AppName}}/reset-telemetry{{if .Period}}?period={{.Period}}{{end}}">
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-sm btn-danger">Reset Telemetry</button>
<button type="submit" class="btn btn-sm btn-danger" data-confirm="Delete all telemetry data for {{.AppName}}? This cannot be undone.">Reset Telemetry</button>
</form>
</div>
<div class="info-grid">
@@ -270,7 +270,7 @@
</table>
<div style="display: flex; gap: 0.5rem; margin-top: 0.75rem;">
<button type="submit" class="btn btn-sm btn-danger" onclick="document.getElementById('issueAction').value='selected';">Dismiss Selected</button>
<button type="submit" class="btn btn-sm btn-danger" onclick="if(!confirm('Dismiss ALL issues for {{.AppName}}? They resurface on a new occurrence.')) return false; document.getElementById('issueAction').value='all';">Dismiss All Issues</button>
<button type="submit" class="btn btn-sm btn-danger" onclick="document.getElementById('issueAction').value='all';" data-confirm="Dismiss ALL issues for {{.AppName}}? They resurface on a new occurrence.">Dismiss All Issues</button>
</div>
</form>
<script>
@@ -5,6 +5,7 @@
<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?v={{hubVersion}}">
{{template "inline_confirm_js"}}
</head>
<body>
{{template "icon_sprite"}}
@@ -123,18 +123,18 @@
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.')">
data-confirm="Re-issue the offsite credentials? The 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>
{{if eq (index . "type") "shared"}}
<!-- SLICE 4: operator freeze lever (readonly on the sub-account) — MANUAL only, never
automatic: freezing also blocks prune, the customer's only way down from over-quota. -->
<button type="submit" class="btn btn-outline" style="margin-top:.5rem"
formaction="/configs/{{$.Config.CustomerID}}/offsite-freeze" formmethod="POST"
onclick="return confirm('Freeze the offsite storage (read-only)?\n\nNew backups AND prune will fail until unfrozen — use for runaway usage only.')">
data-confirm="Freeze the offsite storage (read-only)? New backups AND prune will fail until unfrozen — use for runaway usage only.">
Freeze offsite (read-only)</button>
<button type="submit" class="btn btn-outline" name="unfreeze" value="1" style="margin-top:.5rem"
formaction="/configs/{{$.Config.CustomerID}}/offsite-freeze" formmethod="POST"
onclick="return confirm('Unfreeze the offsite storage (read-write again)?')">
data-confirm="Unfreeze the offsite storage (read-write again)?">
Unfreeze offsite</button>
{{end}}
{{end}}{{end}}{{end}}
@@ -177,7 +177,7 @@
Rides the parent form via formaction; _csrf submits with it. -->
<button type="submit" class="btn btn-outline" style="margin-top:.5rem"
formaction="/configs/{{.Config.CustomerID}}/pbsdr-reissue" formmethod="POST"
onclick="return confirm('Re-issue the PBS credentials?\n\nThe endpoint token is re-keyed and a fresh one-time secret is staged for the host agent. A box with a working storage entry re-applies on its next desired-state fetch.')">
data-confirm="Re-issue the PBS credentials? The endpoint token is re-keyed and a fresh one-time secret is staged for the host agent. A box with a working storage entry re-applies on its next desired-state fetch.">
Re-issue PBS credentials</button>
{{end}}
</details>
@@ -7,6 +7,7 @@
<link rel="stylesheet" href="/style.css?v={{hubVersion}}">
<meta name="csrf-token" content="{{.CSRFToken}}">
<script>function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}</script>
{{template "inline_confirm_js"}}
</head>
<body>
{{template "icon_sprite"}}
@@ -405,10 +406,9 @@
</div>
<span class="form-hint">The per-customer secret that fetches the whole config — masked by default; never place it on a command line (the installer reads it at a no-echo prompt).</span>
</div>
<form method="POST" action="/configs/{{.CustomerID}}/regen-password" style="margin-top: 0.5rem;"
onsubmit="return confirm('Regenerate retrieval password? The old password will stop working immediately.')">
<form method="POST" action="/configs/{{.CustomerID}}/regen-password" style="margin-top: 0.5rem;">
{{.CSRFField}}
<button type="submit" class="btn btn-outline btn-sm">Regenerate</button>
<button type="submit" class="btn btn-outline btn-sm" data-confirm="Regenerate retrieval password? The old password will stop working immediately.">Regenerate</button>
</form>
</div>
<div class="credential-row" style="margin-top: 1rem;">
@@ -446,10 +446,9 @@
<span class="form-hint">The customer sets + owns the dashboard password (claim code → own password). The code goes ONLY to the registered address ({{.Email}}); the hub stores a hash — no plaintext code exists to display.</span>
</div>
{{if .Claim}}
<form method="POST" action="/configs/{{.CustomerID}}/claim-resend" style="margin-top: 0.5rem;"
onsubmit="return confirm('Send a fresh code to the registered address? The previous code stops working immediately.')">
<form method="POST" action="/configs/{{.CustomerID}}/claim-resend" style="margin-top: 0.5rem;">
{{.CSRFField}}
<button type="submit" class="btn btn-outline btn-sm">{{if .Claim.ClaimedAt}}Visszaállító kód küldése{{else}}Kód újraküldése{{end}}</button>
<button type="submit" class="btn btn-outline btn-sm" data-confirm="Send a fresh code to the registered address? The previous code stops working immediately (the box activates it on its next report, ~15 min).">{{if .Claim.ClaimedAt}}Visszaállító kód küldése{{else}}Kód újraküldése{{end}}</button>
</form>
{{end}}
</div>
@@ -713,16 +712,14 @@
<button type="submit" class="btn btn-sm">Unblock</button>
</form>
{{else}}
<form method="POST" action="/customers/{{.CustomerID}}/block" style="display:inline"
onsubmit="return confirm('Block this customer? They will be hidden from the Dashboard.')">
<form method="POST" action="/customers/{{.CustomerID}}/block" style="display:inline">
{{.CSRFField}}
<button type="submit" class="btn btn-outline btn-sm">Block</button>
<button type="submit" class="btn btn-outline btn-sm" data-confirm="Block this customer? They will be hidden from the Dashboard.">Block</button>
</form>
{{end}}
<form method="POST" action="/configs/{{.CustomerID}}/delete" style="display:inline"
onsubmit="return confirm('Delete configuration for {{.CustomerID}}? This cannot be undone.')">
<form method="POST" action="/configs/{{.CustomerID}}/delete" style="display:inline">
{{.CSRFField}}
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
<button type="submit" class="btn btn-danger btn-sm" data-confirm="Delete configuration for {{.CustomerID}}? This cannot be undone.">Delete</button>
</form>
</div>
</section>
@@ -1047,7 +1044,8 @@
if (document.getElementById('gen-controls')) { genUpdate(); }
function disableGeo(customerID) {
if (!confirm('Összes geo-korlátozás eltávolítása?\n\nEz közvetlenül törli a Cloudflare WAF szabályokat és értesíti a controllert.')) return;
var geoBtn = document.getElementById('btn-geo-disable');
felhomConfirm(geoBtn, 'Összes geo-korlátozás eltávolítása? Ez közvetlenül törli a Cloudflare WAF szabályokat és értesíti a controllert.', function () {
var btn = document.getElementById('btn-geo-disable');
var msg = document.getElementById('geo-msg');
btn.disabled = true;
@@ -1076,6 +1074,7 @@
btn.disabled = false;
btn.textContent = 'Összes geo-korlátozás eltávolítása';
});
});
}
{{if .HasConfig}}
// Load YAML preview
@@ -0,0 +1,32 @@
{{define "inline_confirm_js"}}
<script>
/* Inline two-step confirm (take-two F-16, the hub siblings of drill F-11): the trigger swaps IN
PLACE to "<question> Igen/Mégse". Never native confirm() — an OS-modal dialog freezes browser
automation (CDP) and blocks the session. Form buttons opt in with data-confirm="…" (submitted
via requestSubmit so formaction/name-value are honored); JS flows call
felhomConfirm(el, question, onYes) directly. NOT for the danger-zone typed-confirm cascade —
that heavyweight flow stays as is. */
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-sm btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-sm btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
</script>
{{end}}
+4
View File
@@ -975,6 +975,10 @@ header h1 span { color: var(--blue-bright); }
}
body.js-tabs .tab-panel:not(.tab-panel-active) { display: none; }
/* Inline two-step confirm (take-two F-16) — the in-place replacement for native confirm() */
.inline-confirm { display: inline-flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.inline-confirm-q { font-size: .8rem; color: var(--warn); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation: none !important; transition: none !important; }
}
+36
View File
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
"""Hub native-confirm gate (take-two F-16, the hub sibling of drill F-11) — native confirm()/
prompt() dialogs are OS-modals that freeze browser automation (CDP) and are banned from the hub
UI; consequential buttons use the inline felhomConfirm helper (templates/inline_confirm.html).
The danger-zone typed-confirm flow does not use native dialogs either.
Run from the repo root: python scripts/hub_confirm_gate.py
Exit 1 if any native confirm(/prompt( call remains in hub templates.
"""
import io, os, re, sys
ROOT = os.path.join("hub", "internal", "web", "templates")
# A bare confirm(/prompt( CALL with an argument: not preceded by an identifier character, so felhomConfirm(
# never matches. window.confirm( still matches ('.' is not an identifier char).
NATIVE = re.compile(r"(?<![A-Za-z0-9_$])(?:confirm|prompt)\(\s*[^)\s]")
def main():
total = 0
for fn in sorted(os.listdir(ROOT)):
if not fn.endswith(".html"):
continue
path = os.path.join(ROOT, fn)
for lineno, line in enumerate(io.open(path, encoding="utf-8"), 1):
if NATIVE.search(line):
total += 1
print("%s:%d %s" % (fn, lineno, line.strip()[:120].encode('ascii', 'backslashreplace').decode()))
if total:
print("HUB CONFIRM GATE FAILED: %d native confirm()/prompt() call(s) remain" % total)
sys.exit(1)
print("hub confirm gate OK — no native confirm()/prompt() in hub templates")
if __name__ == "__main__":
main()